Next.jsのmiddlewareで複数Webアプリを1つのLIFFアプリのように扱う
LIFFアプリの参照先を複数にまたがらせたい
同じLIFFだけれど、たとえば特定パスのみ別でホスティングされているWebアプリにつなげたいなどのケースを考えます。
認証スコープなどの問題も増えますし、あまりフロントエンドのアプリを分割ホストしたくはないですが、いつの時代もやんごとなき理由はあるでしょう。
1次リダイレクトと2次リダイレクト
LIFFの公式ドキュメントに、LIFF初期化時のリダイレクトフローについて記載があります。
いったん用語を整理しましょう。
- エンドポイントURL:自身がホストするアプリのURL(LINE Developer Consoleで入力するもの)
- LIFF URL:
https://liff.line.me
で始まるURL(LIFFアプリごとに付与されるもの)- ミニアプリの場合は
https://miniapp.line.me
- ミニアプリの場合は
で、例を見たほうが早いので雑にまとめると、
# アプリケーションルート
Endpoint URL: https://example.com/
LIFF URL: https://liff.line.me/{liffId}/
# 2次リダイレクトあり(パスあり)
Endpoint URL: https://example.com/foo/bar
LIFF URL: https://liff.line.me/{liffId}/foo/bar
パスありのページは常に2次リダイレクトされる
まずLIFFで把握すべきポイントとして、パスありのページにアクセスすると、必ず2次リダイレクトされます。
つまり、最初に1次リダイレクトでアプリケーションルートに到達し、liff.init()
が呼び出されます。
そのliff.init()
の中でliff.state
というクエリパラメータに2次リダイレクト先を保持しながら2次リダイレクトをおこなう構成です。
複数アプリケーションを1つのLIFFアプリのように扱う
ここまでの話を踏まえて、複数アプリケーションを1つのLIFFアプリのように扱う方法を考えてみましょう。
- 単一のエンドポイントURLを用意する
- エンドポイントURLに来たアクセスを適切なアプリに振り分ける
ここでは例として、
- 2つのNext.jsアプリケーションを用意し、
- 振り分けをmiddlewareでおこなう
という構成を考えます。
1つ目のアプリケーションをBASE
とし、LIFFとしての実装に加え、middlewareを含めます。
もう1つのアプリケーションはTARGET
とし、特定パスの場合にのみ利用されることを想定します。
単にmiddlewareで特定パスをリダイレクトした場合
以下のように、単純に特定パスの場合にリダイレクトするだけだとエラーとなります。
if (url.pathname.startsWith("/target")) {
const proxiedUrl = `https://target.example.com${url.pathname}${url.search}`;
return NextResponse.redirect(proxiedUrl);
}
- BASEのLIFF URLにアクセス:
/target/foo
- LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:
/?liff.state=%2Ftarget%2Ffoo
- BASEのrootで
liff.init()
が呼び出される - 2次リダイレクトが発生:
/target/foo
- BASEのmiddlewareでパスmatchしてTARGETへリダイレクト
- TARGET上では1次リダイレクトの
liff.init()
が未実施のため、エラーが発生
liff.stateの内容も判定条件に含める場合
2次リダイレクトは、クエリパラメータに条件が含まれてしまうので、以下のような対処が必要です。
export function middleware(req: NextRequest) {
const url = req.nextUrl.clone();
// 2次リダイレクトの場合、liff.stateに2次リダイレクト先が含まれる
const liffState = url.searchParams.get("liff.state");
if (liffState && url.pathname === "/") {
const decoded = decodeURIComponent(liffState);
if (decoded.startsWith("/target")) {
const proxiedUrl = `https://target.example.com${url.search}`;
return NextResponse.redirect(proxiedUrl);
}
}
// それ以外のパスの場合はそのままリダイレクトする
if (url.pathname.startsWith("/target")) {
const proxiedUrl = `https://target.example.com${url.pathname}${url.search}`;
return NextResponse.redirect(proxiedUrl);
}
return NextResponse.next();
}
フローは以下のように変化します。
- BASEのLIFF URLにアクセス:
/target/foo
- LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:
/?liff.state=%2Ftarget%2Ffoo
- BASEのmiddlewareでliff.stateの内容を判定してTARGETへリダイレクト
-
TARGETのrootで
liff.init()
が呼び出される(2次リダイレクト用パラメーターつき) - 2次リダイレクトが発生:
/target/foo
- TARGETアプリ内でのパス遷移のため問題なく動作
withLoginOnExternalBrowser
が有効な場合
このとき、TARGETアプリ側でliff.init()
のオプションであるwithLoginOnExternalBrowser
を有効にしてしまうと、LIFFブラウザ以外では不具合が起こりそうです。
というのも外部ブラウザでのアクセス時は、ログインのリダイレクト戻りが強制で1次リダイレクト先のrootパスのため、未ログイン状態でアクセスすると2次リダイレクトが発生しなくなってしまいます。
- BASEのLIFF URLにアクセス:
/target/foo
- LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:
/?liff.state=%2Ftarget%2Ffoo
- BASEのmiddlewareでliff.stateの内容を判定してTARGETへリダイレクト
-
TARGETのrootで
liff.init()
が呼び出される(2次リダイレクト用パラメーターつき) -
withLoginOnExternalBrowser
により強制ログインが発火 - このときの戻り先が強制的に1次リダイレクト先であるBASEのrootになる
TARGETアプリ側でログインボタンを押させる場合
リダイレクト戻りのURLはBASE側を指定しないとredirectUri mismatchでLINE側ドメイン遷移時に400エラーとなるので注意が必要です。
(LINE Developer CondoleでLIFFのエンドポイントURLに指定するとLINEログインのリダイレクト戻り先としてホワイトリスト登録される)
liff.login({ redirectUri: 'https://base.example.com/target' });
また、LIFFでは、どのLIFF IDに対してのリダイレクトなのかまではホワイトリスト判定で行っていないように見受けられます。
そのためTARGETのURLを指定した別のLIFFが同一プロバイダにあればホワイトリスト登録されて通ると思いますが、これは特に試していません(興味がある方はご自身で検証してみてください)。
おわりに
今回は簡易的にNext.jsのmiddlewareでやりましたが、リバプロ背後にアプリ配置したい場合などでも同様かと思います。
LIFFアプリの場合、適切にliff.init()
される必要性があるため、単純にリバプロでrewriteするだけでは不十分です。
今回の分割ケースに限らず、LIFFでは2次リダイレクトについて把握することが重要でしょう。
Discussion