3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Auth0のPost-login Actionで任意のWebページを表示し、認証フローを継続する

3
Posted at

Auth0 Post-login Actionでログイン途中に独自画面を挟み、/continueで認証フローに戻す

Auth0 を使っていると、ログイン成功直後に

  • 独自の案内画面を 1 回だけ表示したい
  • アプリ固有の後続処理を済ませてからログイン完了にしたい
  • Auth0 の標準画面だけでは扱いづらい UI やブラウザ操作を挟みたい

といった要件にぶつかることがあります。

この記事では、Auth0 の Post-login Action から任意の Web ページへ遷移し、その後に元のログインフローへ戻して継続する方法をまとめます。

ポイントは、callback 後にアプリ側で画面を出すのではなく、Auth0 のログインフローの途中で一度画面を挟むことです。

この記事で扱う前提

この記事では、外部画面の背後にサーバー側の処理を持つ構成を前提にします。

署名付きトークンの検証や、Auth0 に返す追加データの署名はサーバー側で行います。
ブラウザ側には、署名用・検証用のシークレットを置きません。

やりたいこと

今回扱うのは、次のような要件です。

  • Auth0 ログイン後に、独自のフロント画面を一度表示したい
  • その画面で案内、確認、追加処理などを行いたい
  • 画面表示後は、元の Auth0 ログインフローへ戻したい
  • 最終的には、通常どおり元の遷移先サービスへ戻したい

たとえば、以下のようなケースを想定しています。

  • Auth0 の標準画面だけでは扱いづらいアプリ固有の画面を挟みたい
  • Auth0 では持たない自社システム側の状態を確認・保存したい
  • ブラウザ上で追加操作を行ってからログインを完了したい

先に結論

Auth0 では、Post-login Action + api.redirect.sendUserTo() + /continue?state=... を使うことで、この構成を実現できます。

大まかな流れは以下です。

  1. Auth0 の Post-login Action で条件判定する
  2. 条件に一致した場合、api.redirect.sendUserTo() で任意の Web ページへ遷移する
  3. Auth0 は遷移先 URL に state を付与する
  4. 外部画面側で必要な表示や処理を行う
  5. https://<Auth0のドメイン>/continue?state=... に戻す
  6. Auth0 が同じ認証トランザクションを再開する
  7. 最後に通常どおり元の redirect_uri へ callback される

つまり、「ログイン途中で一度寄り道し、同じ認証フローに戻る」 という構成です。

処理のフロー

全体の遷移をシーケンス図で整理すると、以下のようになります。

まず押さえる前提

Auth0 の Post-login Action では、ログイン成功後、アプリケーションへの callback が完了する前のタイミングで処理を実行できます。

このタイミングで api.redirect.sendUserTo() を呼ぶと、Auth0 はログインフローを一時停止し、指定した外部ページへユーザーをリダイレクトします。

その際、Auth0 は外部ページの URL に state パラメータを付与します。

https://example.com/custom-step?state=abc123

この state は、単なる画面表示用のパラメータではありません。
Auth0 が同じ認証トランザクションを再開するためのキーです。

また、state は Auth0 が発行する不透明な値であり、CSRF 対策の文脈でも重要な値です。

そのため、外部画面側で中身を解釈したり、独自の値に置き換えたりせず、受け取った値をそのまま /continue に返します。

https://<Auth0のドメイン>/continue?state=<受け取ったstate>

state が欠けていたり、別の値に変わっていたりすると、Auth0 は元のログインコンテキストを復元できません。

実装方針

実装パターンは大きく 2 つあります。

パターンA: 外部画面から Auth0 に追加データを返さない

単に「ログイン途中で画面を 1 回表示したい」だけであれば、このパターンが一番シンプルです。

外部画面側で必要な表示や処理を行ったあと、受け取った state を使って /continue に戻します。

https://<Auth0のドメイン>/continue?state=<受け取ったstate>

外部画面での処理結果は、自社システム側に保存しておき、Auth0 には返さない構成です。

実務では、この構成で十分なケースも多いです。

パターンB: 外部画面から Auth0 に追加データを返す

外部画面での処理結果を onContinuePostLogin で使いたい場合は、追加データを署名付きトークンとして Auth0 に返します。

この場合は、外部画面から /continue に戻す際に、result_token のようなパラメータを渡し、Action 側で api.redirect.validateToken() を使って検証します。

この記事では、state は query parameter に、result_token は POST body に返す構成で説明します。

POST https://<Auth0のドメイン>/continue?state=<受け取ったstate>

result_token はサーバー側で生成します。
署名付きトークンを生成するためのシークレットは、ブラウザに置けないためです。

実装の手順

1. Post-login Action で外部画面へ遷移する

まず、Post-login Action で対象かどうかを判定します。

たとえば、ユーザーの app_metadata を見て、追加画面を表示するかどうかを決めます。

条件に一致した場合は、api.redirect.sendUserTo() で任意の Web ページへ遷移します。

exports.onExecutePostLogin = async (event, api) => {
  if (!shouldShowCustomStep(event)) {
    return;
  }

  const screenToken = api.redirect.encodeToken({
    secret: event.secrets.SCREEN_TOKEN_SECRET,
    expiresInSeconds: 300,
    payload: {
      sub: event.user.user_id,
      step: "custom-step",
    },
  });

  api.redirect.sendUserTo("https://example.com/custom-step", {
    query: {
      screen_token: screenToken,
    },
  });
};

function shouldShowCustomStep(event) {
  return event.user.app_metadata?.has_completed_custom_step !== true;
}

ここで screen_token を渡しているのは、外部画面側で必要な情報を受け取りつつ、サーバー側で改ざんされていないことを検証できるようにするためです。

Auth0 は sendUserTo() で指定した URL に、screen_token に加えて state を付与します。

結果として、外部画面には以下のような URL で遷移します。

https://example.com/custom-step?screen_token=...&state=...

署名に使うシークレット文字列はコードに直接書かず、Auth0 の Action Secrets に登録し、event.secrets 経由で参照します。

なお、署名付きJWTは暗号化とは別物です。

クエリパラメータに載せる値には、パスワード、認証情報、個人情報など、漏れて困る機密情報を含めないようにします。

2. 外部画面で statescreen_token を受け取る

外部画面では、Auth0 から渡されたクエリパラメータを受け取ります。

  • state
  • screen_token

state はフロー再開用です。

screen_token は、必要に応じてサーバー側で検証し、ユーザー識別や画面表示条件の確認に使います。

外部画面は、通常のログインガードを通さずに到達できる専用ルートとして用意します。

ただし、無条件に処理を進めてよいという意味ではありません。

必要なのは、Auth0 の認証完了前でも到達できることであって、誰でも自由に処理を進められることではありません。

少なくとも、サーバー側では以下を確認した方が安全です。

  • screen_token の署名が正しいこと
  • screen_token が期限切れでないこと
  • sub が想定どおりであること
  • step === "custom-step" など、用途を表す claim が正しいこと
  • 必要に応じて、一度きり利用の消費管理を行うこと

ブラウザ側には、署名用・検証用のシークレットを置きません。

3. 外部画面から Auth0 フローへ戻す

外部画面で必要な表示や処理が終わったら、Auth0 の /continue に戻します。

追加データを Auth0 に返さない場合は、state だけを返します。

const continueUrl =
  `https://<Auth0のドメイン>/continue?state=${encodeURIComponent(state)}`;

window.location.assign(continueUrl);

追加データを Auth0 に返したい場合は、サーバー側で署名付き result_token を生成し、/continue に渡します。

<form method="post" action="https://<Auth0のドメイン>/continue?state=<受け取ったstate>">
  <input type="hidden" name="result_token" value="<署名付きトークン>" />
  <button type="submit">続行</button>
</form>

返却用トークンには state claim を含めます。

{
  "sub": "auth0|xxxxxxxx",
  "state": "<Auth0から受け取ったstate>",
  "result": "accepted",
  "exp": 1234567890
}

state claim を含めることで、Auth0 側で「このトークンが今回のリダイレクトに対応するものか」を検証できます。

また、sub も含めておくことで、再開後の Action 側で現在のユーザーとトークンの対象ユーザーが一致しているか確認できます。

4. フロー再開後に後続処理を続ける

/continue に戻った後は、同じ Action 内の onContinuePostLogin が実行されます。

以下は、外部画面から result_token を返すパターンBの例です。
パターンAのように state だけを返す場合は、validateToken() による result_token の検証は不要です。

ここでは、たとえば以下のような処理を行えます。

  • app_metadata を更新して「処理済み」にする
  • api.authentication.recordMethod() でカスタム認証メソッド完了を記録する
  • 外部画面側から返されたトークンを検証し、結果を後続フローに反映する
  • 必要に応じてログインを拒否する
const CUSTOM_METHOD_URL = "https://example.com/custom-step";

exports.onExecutePostLogin = async (event, api) => {
  if (!shouldShowCustomStep(event)) {
    return;
  }

  const screenToken = api.redirect.encodeToken({
    secret: event.secrets.SCREEN_TOKEN_SECRET,
    expiresInSeconds: 300,
    payload: {
      sub: event.user.user_id,
      step: "custom-step",
    },
  });

  api.redirect.sendUserTo(CUSTOM_METHOD_URL, {
    query: {
      screen_token: screenToken,
    },
  });
};

exports.onContinuePostLogin = async (event, api) => {
  let payload;

  try {
    payload = api.redirect.validateToken({
      secret: event.secrets.RESULT_TOKEN_SECRET,
      tokenParameterName: "result_token",
    });
  } catch (error) {
    api.access.deny("Invalid token");
    return;
  }

  if (payload.sub !== event.user.user_id) {
    api.access.deny("Token subject mismatch");
    return;
  }

  if (payload.result !== "accepted") {
    api.access.deny("Custom step was not completed");
    return;
  }

  api.user.setAppMetadata("has_completed_custom_step", true);
  api.authentication.recordMethod(CUSTOM_METHOD_URL);
};

function shouldShowCustomStep(event) {
  return event.user.app_metadata?.has_completed_custom_step !== true;
}

onContinuePostLogin は、api.redirect.sendUserTo() を呼んだ Action と同じ Action 内に定義します。

別の Action に分けて書くのではなく、リダイレクト開始と再開後の処理を同じ Action に置くのがポイントです。

なお、api.authentication.recordMethod()onContinuePostLogin で使う API です。

「このセッションでは、このカスタムステップを完了済み」と記録したい場合に使えます。

仕組みのポイント

sendUserTo() は「寄り道」、/continue は「復帰」

役割を分けて理解するとシンプルです。

  • api.redirect.sendUserTo()
    • 任意の Web ページへ一度遷移する
  • /continue?state=...
    • 元の Auth0 フローを再開する

sendUserTo() を呼ぶと、Auth0 の Actions pipeline は一時停止します。

外部画面から /continue に戻ると、同じ Action の onContinuePostLogin から処理が再開されます。

通常の access token / ID token は前提にしない

外部画面へ遷移する時点では、Auth0 の認証トランザクションはまだ完了していません。

そのため、アプリが callback 後に受け取る通常の access token / ID token を前提に本人確認する設計にはしない方が安全です。

必要な情報は、api.redirect.encodeToken() などで別途渡します。

また、外部画面側から Auth0 に処理結果を返したい場合は、サーバー側で署名したトークンを使い、onContinuePostLogin 側で api.redirect.validateToken() により検証します。

一度だけ表示したい場合は別途制御が必要

「初回だけ表示」「未表示ユーザーだけ表示」のような要件では、表示済みかどうかを別途管理します。

たとえば、以下のような方法があります。

  • app_metadata で表示済みフラグを持つ
  • api.authentication.recordMethod() で同一セッション内の完了状態を記録する
  • 自社システム側に処理済み状態を保存する

恒久的に「初回だけ」を表現したい場合は、app_metadata や自社システム側など、セッションをまたいで参照できる場所に状態を持つ必要があります。

実務上の注意

フロントチャネルに機密情報を載せない

screen_tokenresult_token は、URL やフォームを通じてブラウザを経由します。

そのため、フロントチャネルに載せる情報は最小限にします。

特に、以下のような情報は入れない方が安全です。

  • パスワード
  • アクセストークン
  • リフレッシュトークン
  • 秘密鍵やAPIキー
  • 不要な個人情報

署名付きJWTは「改ざん検知」には使えますが、「中身を読まれないこと」を保証するものではありません。

署名や状態変更はサーバー側で扱う

ブラウザ側に、署名用・検証用のシークレットを置くことはできません。

そのため、以下のような処理はサーバー側で行います。

  • screen_token の署名検証
  • result_token の生成
  • 一度きり利用の消費管理
  • 自社システム側への処理結果保存
  • ユーザーIDと処理対象の突き合わせ

ブラウザ側は、画面表示とユーザー操作を担当し、信頼が必要な処理はサーバー側で行う構成にすると安全です。

ログインフロー中に重い処理を入れすぎない

この方式はログインフローの途中に処理を挟むため、外部APIの遅延や失敗がそのままログイン体験に影響します。

そのため、ログイン完了前に本当に必要な処理だけをこの方式で扱うのが安全です。

たとえば、必須ではない通知送信や集計処理などは、callback 後や非同期処理に逃がすことを検討します。

向いているケース

この方式は、Auth0 の標準画面だけでは完結しにくい処理を、ログインフロー中に挟みたい場合に向いています。

たとえば、次のようなケースです。

  • ログイン直後に、アプリ固有の状態確認を行いたい
  • Auth0 標準画面では表現しにくい独自 UI や既存フロント部品を使いたい
  • スクリーンショット取得、ローカル保存、ファイル選択など、ブラウザ側の追加操作を伴う
  • Auth0 では持たないアプリ固有データを参照しながら画面表示や分岐を行いたい
  • 画面表示だけでなく、その結果を自社システム側へ反映した上でログインフローを継続したい

逆に、Auth0 Forms や標準画面だけで完結する要件であれば、無理に外部ページへ出さず、Auth0 側で閉じる方がシンプルです。

また、ログイン完了後に実行しても問題ない処理であれば、callback 後のアプリ側で扱う方が実装も運用もシンプルになることがあります。

使えない・注意が必要なケース

Redirect Actions は、ブラウザでのリダイレクトを前提にした仕組みです。

そのため、すべての認証フローで使えるわけではありません。

特に以下のようなケースでは注意が必要です。

  • ROPG などの非対話フロー
  • Refresh Token exchange
  • prompt=none を使うサイレント認証

たとえば、ROPG のようにユーザーがブラウザ上のリダイレクトフローにいない場合、この方式は使えません。

また、Refresh Token exchange はバックチャネルで行われるため、ユーザーを外部画面へリダイレクトする前提と合いません。

prompt=none はユーザー操作なしで認証を完了させるための指定です。

そのため、Redirect Action によってユーザー操作が必要な画面へ遷移しようとすると、interaction_required になります。

よくあるつまずき

state を正しく返していない

/continue?state=... に返す state が欠けていたり、別の値になっていたりすると、元のフローに復帰できません。

まずは、Auth0 から受け取った state をそのまま返しているかを確認します。

外部画面を通常の認証ミドルウェア配下に置いてしまう

外部画面を普段のログイン必須ルートに置くと、そこに到達した時点でまた認証導線に入り、別のトランザクションが始まることがあります。

外部画面は、通常のアプリ認証ガードの対象外にした専用ルートとして用意する方が安全です。

ブラウザ側で result_token を署名しようとする

外部画面から Auth0 に追加データを返す場合、result_token のような署名付きトークンを返す構成があります。

ただし、署名に使うシークレットをブラウザに置くことはできません。

そのため、ブラウザ側だけで result_token を安全に生成することはできません。

追加データを Auth0 に返したい場合は、サーバー側で署名付きトークンを生成します。

onContinuePostLogin を別Actionに書いてしまう

/continue 後の処理は、リダイレクトを発行した同じ Action の onContinuePostLogin で再開されます。

そのため、onExecutePostLoginonContinuePostLogin は同じ Action に置きます。

ログイン元アプリの redirect 設定に引っ張られる

最終的な戻り先は、ログイン元アプリの redirect_uri や callback 後のリダイレクトロジックに依存します。

差し込み画面自体が正しく動いていても、その後の戻り先設定次第で想定外の画面へ遷移することがあります。

ローカル検証時の注意

これは本番構成そのものの話ではありませんが、ローカルで検証する際は少し注意が必要です。

たとえば localhost で複数アプリをポート違いで立ち上げると、cookie が混線しているように見えることがあります。

ブラウザ cookie は port ではなく host 単位で扱われるため、別アプリの cookie が見えてしまうことがあります。

また、モックアプリや検証用アプリの callback 後リダイレクト先が別アプリを向いていると、Auth0 の仕組み自体ではなく、ローカル構成の都合で誤遷移して見えることがあります。

ローカルでの挙動が不安定なときは、以下をブラウザの Network タブで確認すると切り分けしやすいです。

  • Auth0 から外部画面に渡された state
  • 外部画面から /continue に返している state
  • callback 後の Location
  • 各アプリの cookie
  • 認証開始元の redirect_uri

まとめ

Auth0 の Post-login Action を使うと、ログイン途中で任意の Web ページを一度表示し、その後に同じ認証フローへ戻すことができます。

実装の基本は次の流れです。

  • Action で api.redirect.sendUserTo() する
  • Auth0 が外部画面に state を付けて遷移する
  • 外部画面で必要な表示や処理を行う
  • /continue?state=... に戻す
  • 必要なら追加データは署名付きトークンで Auth0 に返す
  • onContinuePostLogin で後続処理を行う

このパターンは、Auth0 の標準画面だけでは完結しない処理を、ログインフローに自然に組み込みたいときに有効です。

一方で、通常の callback 後画面とは前提が異なります。

特に、以下の点は実装前に押さえておくと安全です。

  • state は必ず元の値を /continue に返す
  • 外部画面はログインガード配下に置かない
  • callback 後の access token / ID token を前提にしない
  • ブラウザ側で署名付きトークンを生成・検証しない
  • 署名付きJWTに機密情報を入れない
  • result_token を使う場合は sub も確認する
  • onContinuePostLogin は同じ Action に書く
  • ログインフロー中に重い処理を入れすぎない
  • リダイレクトに向かないフローでは使わない

参考

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?