Auth0のActionsで特定の操作のときだけMFAを求め、一定時間は省略するStep-up認証を試してみた

Auth0のActionsで特定の操作のときだけMFAを求め、一定時間は省略するStep-up認証を試してみた

Auth0のActionsを使って、ユーザー情報変更などの重要な操作のときだけMFAを求め、一度通したら一定時間は省略するStep-up認証を実装してみました。実装のポイントと、アプリケーション側で気を付けるべき判定方法をご紹介します。
2026.11.10

こんにちは。

ゲームソリューション部/業務効率化ソリューション部の西川です。

今回はAuth0のActionsで、ユーザー情報の変更のような特定の操作のときだけMFAを求め、MFAを通してから一定時間はMFAを省略するStep-up認証を試してみました。

はじめに

背景

ログインのたびにMFAを求めると、利用者の手間が増えます。一方で、メールアドレスやパスワードの変更のような操作は、パスワードだけで通したくありません。
そこで、通常のログインではMFAを求めず、重要な操作の直前にだけMFAを求める、という要件がよく挙がります。

この要件には、一度MFAを通したら一定時間はMFAを省略したい、という条件もよく付きます。重要な操作を続けて行うたびにMFAを求めると、結局は利用者の手間が増えるためです。

今回は、この要件をAuth0のActionsで実装し、動きを確かめました。

Step-up認証とは

公式ドキュメント(Add Step-up Authentication)によると、Step-up認証は、機密性の高い操作に対して、より強い認証手段での認証をアプリケーションが利用者に求める仕組みです。

Webアプリケーションでの構成はConfigure Step-up Authentication for Web Appsに書かれています。
アプリケーションは認可リクエストにacr_valuesを付け、Actionがそれを見てMFAを要求します。アプリケーションは、返ってきたIDトークンのamrクレームにmfaが入っているかで、MFAを通したかを判定します。

今回は、この構成に「最後にMFAを通した時刻から一定時間はMFAを省略する」判定を足しました。

試した環境

  • Auth0の検証用テナント(デモ環境)
  • MFAの要素は、ワンタイムパスワード、Auth0 Guardianのプッシュ通知、電話メッセージ、回復コードが有効
  • サンプルアプリはNode.js 26、Express 4、express-openid-connect 2.20で作り、手元のhttp://stepup.localhost:3000で動かしました
  • 動きを確かめやすいように、MFAを省略する時間は5分にしています
  • 画像に写るテナント名とメールアドレスは置き換えて掲載しています

やってみた

MFAのポリシーを確認する

左メニューの「セキュリティ」から「多要素認証」を開きます。使う要素が有効になっていることを確認します。
今回の検証用テナントでは、ワンタイムパスワードが有効になっていました。

多要素認証の画面で、ワンタイムパスワードと回復コードが有効になっている

同じ画面の「ポリシーを定義する」で、「多要素認証を要求する」を「検証しない」にします。
こうしておくと、通常のログインではMFAが求められません。重要な操作のときだけ、ActionでMFAを要求します。

ポリシーを定義するで、多要素認証を要求するが検証しないになっている

画面の説明にもあるとおり、MFAに影響するActionは、この設定より優先されます。

アプリケーションを作る

「アプリケーション」から「アプリケーションを作成」を押し、種類に「一般的なWebアプリケーション」を選んで作成しました。設定タブで、許可するCallback URLにhttp://stepup.localhost:3000/callback、許可するログアウトURLにhttp://stepup.localhost:3000を入れて保存します。

最初はhttp://localhost:3000で動かしていましたが、重要な操作のたびにリダイレクトするこの構成では、リダイレクトのたびに同意画面が表示されました。
公式ドキュメント(User Consent and Third-Party Applications)によると、localhostのような検証できないコールバックURIを使うアプリケーションでは、ファーストパーティのアプリケーションでも確認の画面が表示されることがあります。
stepup.localhostに変えたところ、本検証環境(Chromeからファーストパーティのアプリケーションにアクセスした場合)では、同意画面は表示されなくなりました。1回だけの観測なので、ブラウザーやテナントの設定によって結果が変わる可能性があります。

Actionを作る

「アクション」の「ライブラリー」で「アクションの作成」から「初めから構築する」を選びます。トリガーは「Login / Post Login」、ランタイムは「Node 22」のまま作成し、次のコードを入れてデプロイしました。

const STEP_UP_ACR = 'http://schemas.openid.net/pape/policies/2007/06/multi-factor';
const MFA_VALID_SECONDS = 5 * 60; // この時間内に MFA を通していれば省略する

exports.onExecutePostLogin = async (event, api) => {
  const mfa = (event.authentication?.methods || []).find((m) => m.name === 'mfa');
  if (mfa) {
    api.idToken.setCustomClaim('https://stepup.example.com/last_mfa_at', mfa.timestamp);
  }

  const requested = (event.transaction?.acr_values || []).includes(STEP_UP_ACR);
  if (!requested) return; // 通常のログインでは MFA を求めない

  const elapsed = mfa ? (Date.now() - new Date(mfa.timestamp).getTime()) / 1000 : Infinity;
  if (elapsed < MFA_VALID_SECONDS) return; // 直近に MFA を通しているので省略する

  api.multifactor.enable('any', { allowRememberBrowser: false });
};

デプロイしたActionのコード

判定に使っているのは次の2つです。

  • event.transaction.acr_valuesには、認可リクエストに付けたacr_valuesが入ります。アプリケーションがMFAを表す値を付けてきたときだけ、MFAを求める対象にします。
  • event.authentication.methodsには、利用者がそのセッションで済ませた認証方法が入ります(Actions Triggers: post-login - Event Object)。本検証環境では、MFAを通したあと、nameがmfaの要素のtimestampにMFAの時刻が入っていました。公式ドキュメントにはこの要素の項目までは書かれていないので、仕様として決まっている値ではない可能性があるので、使う前に自分のテナントで中身を確かめておく必要があります。

api.multifactor.enableのallowRememberBrowserをfalseにしているのは、ブラウザーを記憶してMFAを省略する機能と、今回の5分の判定が重ならないようにするためです(Actions Triggers: post-login - API Object)。

最後にMFAを通した時刻は、カスタムクレームとしてIDトークンに入れています。アプリケーション側の判定で使うためで、理由は後の手順で説明します。

Login Flowに追加する

「アクション」の「トリガー」から「post-login」を開き、右の一覧から作ったActionを「開始」の直後にドラッグして、「適用」を押します。

Login Flowの先頭に作成したActionを追加した状態

検証用テナントのLogin Flowには、MFAに関係しないActionがほかに2つ入っています。

重要な操作の前にacr_valuesを付けて認可リクエストを送る

サンプルアプリには、通常の操作にあたる「プロフィール」と、重要な操作にあたる「ユーザー情報の変更」の2つのページを用意しました。

「ユーザー情報の変更」を開くと、アプリケーションはacr_valuesを付けて、もう一度Auth0に認可リクエストを送ります。
Auth0のセッションが残っているので、ログイン画面は表示されません。MFAを求めるかどうかはActionが判断し、アプリケーションは戻ってきたIDトークンを見て操作を許可するかを決めます。

app.get('/settings', requiresAuth(), (req, res) => {
  const c = req.oidc.idTokenClaims;
  const fresh = Date.now() / 1000 - c.iat < 60;
  if (req.query.stepup !== 'done' || !fresh) {
    return res.oidc.login({
      returnTo: '/settings?stepup=done',
      authorizationParams: { acr_values: STEP_UP_ACR },
    });
  }
  // MFA をこの認可リクエストで通した場合は amr に mfa が入る。
  // Action が MFA を省略した場合は amr に入らないので、Action が入れた最後の MFA 時刻で判定する
  const lastMfaAt = c['https://stepup.example.com/last_mfa_at'];
  const recentMfa = lastMfaAt && Date.now() - new Date(lastMfaAt).getTime() < 5 * 60 * 1000;
  if (!(c.amr || []).includes('mfa') && !recentMfa) {
    return res.status(403).send('MFA が確認できないため、この操作はできません。');
  }
  res.send('MFA を確認できたので、操作を許可しました。');
});

ページには、確認のためにIDトークンのamr、acr、last_mfa_at、iatを表示しています。

通常のログインでMFAが求められないことを確かめる

サンプルアプリでログインし、「プロフィール」を開きました。MFAは求められず、IDトークンにもamrは入っていませんでした。

通常のログインのあと、IDトークンにamrが入っていない

重要な操作でMFAが求められることを確かめる

続けて「ユーザー情報の変更」を開くと、MFAの画面が表示されました。テストユーザーはMFAの要素をまだ登録していなかったので、登録の画面から始まります。

ユーザー情報の変更を開いたときに表示されたMFAの登録画面

「Try another method」からワンタイムパスワードを選び、認証アプリで登録しました。MFAを通すとサンプルアプリに戻り、IDトークンのamrにmfaが、acrにMFAを表す値が入っていました。

MFAを通したあと、amrにmfaが入り、操作が許可された

このIDトークンには、Actionで入れたはずのlast_mfa_atが入っていません。
Actionが動くのはMFAの前で、その時点のevent.authentication.methodsには今回のMFAがまだ含まれていないためと考えられます。公式ドキュメント(Actions Triggers: post-login - API Object)にも、api.multifactor.enableによるMFAの要求はログインのフローの最後に行われると書かれています。

5分以内にもう一度重要な操作をしたときの動き

MFAを通してから約1分後に、もう一度「ユーザー情報の変更」を開きました。
今度はMFAの画面は表示されず、そのままサンプルアプリに戻りました。Actionが、最後のMFAから5分以内と判断して、MFAを省略したためです。

ただし、最初に作ったサンプルアプリは、この操作を拒否しました。戻ってきたIDトークンにはlast_mfa_atはありますが、amrもacrも入っていなかったためです。

MFAが省略されたときのIDトークン。amrが入っておらず、操作が拒否された

本検証環境では、Actionがapi.multifactor.enableを呼ばなかったログインでは、同じセッションで直前にMFAを通していても、IDトークンのamrにmfaは入りませんでした。公式ドキュメントの構成どおりにamrだけで判定すると、MFAを省略した場合は操作を通せません。

そこで、アプリケーションの判定を「amrにmfaが入っている、またはlast_mfa_atが5分以内」に変えました。前の手順のコードは、変えたあとのものです。
変えたあとにもう一度開くと、MFAなしで操作が許可されました。

判定を変えたあと、MFAを省略したまま操作が許可された

5分を過ぎてから重要な操作をしたときの動き

最後にMFAを通してから約5分40秒後に、もう一度「ユーザー情報の変更」を開きました。今度はワンタイムパスワードの入力画面が表示され、MFAがもう一度求められました。

5分を過ぎてから開くと、ワンタイムパスワードの入力画面が表示された

本検証環境では、5分以内なら省略され、5分を過ぎると改めて求められる、というActionの判定どおりの動きを確認しました。それぞれ1回だけの観測です。

分かったこと

アプリケーションがIDトークンでMFAを判定する方法

MFAを省略する仕組みをActionに足す場合は、アプリケーション側の判定も変える必要があります。本検証環境では、MFAを省略したログインのIDトークンにamrが入らなかったためです。
今回はActionから最後のMFAの時刻をカスタムクレームで渡しました。

一方で、MFAを通したログインのIDトークンには、そのカスタムクレームが入りません。Actionの時点ではまだMFAが済んでいないためです。
このときはamrにmfaが入るので、アプリケーションはamrとカスタムクレームのどちらかで判定することになります。

アプリケーションでもカスタムクレームの時刻を確かめているのは、今回のActionが、acr_valuesの有無にかかわらず最後のMFAの時刻をIDトークンに入れるためです。時刻を確かめないと、通常のログインで発行された、古いMFAの時刻を含むIDトークンでも操作が通ってしまいます。
あわせて、アプリケーションはiatが60秒以内のIDトークンだけを受け付け、それより古ければもう一度認可リクエストを送ります。

さいごに

Auth0のActionsで、特定の操作のときだけMFAを求め、一定時間は省略するStep-up認証を試してみました。仕組み自体は短いActionで作れましたが、MFAを省略したときのIDトークンの中身は想定と違っていました。

今回のポイントをまとめると次の通りです。

  • 通常のログインでMFAを求めない設定にし、アプリケーションがacr_valuesを付けたときだけActionでMFAを要求できる
  • 最後にMFAを通した時刻はevent.authentication.methodsから取れ、本検証環境では5分以内ならMFAを省略できた
  • 本検証環境では、MFAを省略したログインのIDトークンにamrのmfaが入らなかった。アプリケーションはamrだけで判定せず、Actionから渡した時刻も見る必要がある。
  • 本検証環境では、MFAを通したログインのIDトークンに、最後のMFAの時刻が入らなかった
  • 本検証環境では、localhostのコールバックURIだとリダイレクトのたびに同意画面が表示された

導入するときは、MFAを省略する時間を、操作の重要度に合わせて決めておくとよさそうです。アプリケーション側の判定では、IDトークンのiatも確かめ、古いIDトークンで重要な操作を通さないようにしておくと安全です。

本記事がAuth0でStep-up認証を検討中の方のご参考になっていれば幸いです。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事