🌴

Next.jsのmiddlewareで複数Webアプリを1つのLIFFアプリのように扱う

に公開

LIFFアプリの参照先を複数にまたがらせたい

同じLIFFだけれど、たとえば特定パスのみ別でホスティングされているWebアプリにつなげたいなどのケースを考えます。

認証スコープなどの問題も増えますし、あまりフロントエンドのアプリを分割ホストしたくはないですが、いつの時代もやんごとなき理由はあるでしょう。

1次リダイレクトと2次リダイレクト

LIFFの公式ドキュメントに、LIFF初期化時のリダイレクトフローについて記載があります。

https://developers.line.biz/ja/docs/liff/opening-liff-app/#redirect-flow

いったん用語を整理しましょう。

  • エンドポイント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アプリのように扱う方法を考えてみましょう。

  1. 単一のエンドポイントURLを用意する
  2. エンドポイントURLに来たアクセスを適切なアプリに振り分ける

ここでは例として、

  • 2つのNext.jsアプリケーションを用意し、
  • 振り分けをmiddlewareでおこなう

という構成を考えます。

1つ目のアプリケーションをBASEとし、LIFFとしての実装に加え、middlewareを含めます。
もう1つのアプリケーションはTARGETとし、特定パスの場合にのみ利用されることを想定します。

単にmiddlewareで特定パスをリダイレクトした場合

以下のように、単純に特定パスの場合にリダイレクトするだけだとエラーとなります。

middleware.ts
if (url.pathname.startsWith("/target")) {
  const proxiedUrl = `https://target.example.com${url.pathname}${url.search}`;
  return NextResponse.redirect(proxiedUrl);
}
  1. BASEのLIFF URLにアクセス:/target/foo
  2. LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:/?liff.state=%2Ftarget%2Ffoo
  3. BASEのrootでliff.init()が呼び出される
  4. 2次リダイレクトが発生:/target/foo
  5. BASEのmiddlewareでパスmatchしてTARGETへリダイレクト
  6. TARGET上では1次リダイレクトのliff.init()が未実施のため、エラーが発生

image

liff.stateの内容も判定条件に含める場合

2次リダイレクトは、クエリパラメータに条件が含まれてしまうので、以下のような対処が必要です。

middleware.ts
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();
}

フローは以下のように変化します。

  1. BASEのLIFF URLにアクセス:/target/foo
  2. LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:/?liff.state=%2Ftarget%2Ffoo
  3. BASEのmiddlewareでliff.stateの内容を判定してTARGETへリダイレクト
  4. TARGETのrootでliff.init()が呼び出される(2次リダイレクト用パラメーターつき)
  5. 2次リダイレクトが発生:/target/foo
  6. TARGETアプリ内でのパス遷移のため問題なく動作

withLoginOnExternalBrowserが有効な場合

このとき、TARGETアプリ側でliff.init()のオプションであるwithLoginOnExternalBrowserを有効にしてしまうと、LIFFブラウザ以外では不具合が起こりそうです。

というのも外部ブラウザでのアクセス時は、ログインのリダイレクト戻りが強制で1次リダイレクト先のrootパスのため、未ログイン状態でアクセスすると2次リダイレクトが発生しなくなってしまいます。

  1. BASEのLIFF URLにアクセス:/target/foo
  2. LINE側が1次リダイレクト先のBASEへパラメータ付きでリダイレクト:/?liff.state=%2Ftarget%2Ffoo
  3. BASEのmiddlewareでliff.stateの内容を判定してTARGETへリダイレクト
  4. TARGETのrootでliff.init()が呼び出される(2次リダイレクト用パラメーターつき)
  5. withLoginOnExternalBrowserにより強制ログインが発火
  6. このときの戻り先が強制的に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