Amazon Cognito カスタム認証で「認証方式の識別」に詰まる理由と回避策:ClientMetadata の制約を突破する

Amazon Cognito のカスタム認証フロー(Custom Auth)を構築する際、電話OTP、メールOTP、ソーシャルログインといった複数の認証方式を動的に切り替えたいケースがある。しかし、認証フローの初期段階である DefineAuthChallenge / CreateAuthChallenge トリガーにおいて「クライアントがどの方式を選択したか」を識別できず、実装がスタックするケースが後を絶たない。

本記事では、この仕様上の制約と、現場で採用すべき現実的な解決策を提示する。

Cognito Custom Auth における「初期トリガー」の制約

Amazon Cognito のカスタム認証は、DefineAuthChallengeCreateAuthChallengeVerifyAuthChallengeResponse の3つの Lambda トリガーで構成される。複数の認証方式を同一ユーザーに提供する場合、最初の AdminInitiateAuth もしくは InitiateAuth のタイミングで「どの方式(OTPか、ソーシャルか等)」を Lambda 側に伝える必要がある。

しかし、ここに大きな落とし穴がある。認証フローの初回呼び出し(sessionが空の状態)では、ClientMetadata が Lambda に渡されない。

  • ClientMetadata の挙動: 通常、クライアント側から任意のパラメータを渡すための仕組みだが、カスタム認証の「最初のトリガー」には反映されない仕様だ。
  • 発生する問題: Googleログインを望むユーザーに対し、判別がつかないために一律で電話OTPを発行・送信してしまうといった、不適切なフローが強制される。

筆者の経験上、この仕様を知らずに設計を進め、結合テストの段階で「フロントエンドから送ったフラグが Lambda で受け取れない」と混乱する開発チームは非常に多い。

これはライブラリのバグではなく、Cognito の設計思想に起因する制約だ。

解決のための3つの戦略

この制約を突破し、ユーザーが選択した認証方式を識別するためには、以下のいずれかのアプローチを採るべきだ。

  • 1. ダミーチャレンジ(NULL_CHALLENGE)の先行実施
    初回呼び出しではあえて認証を行わず、即座に NULL_CHALLENGE を応答する。クライアントがこれに RespondToAuthChallenge で応じる際、改めて ClientMetadata を付与することで、2回目以降のトリガーで確実に情報を渡す手法だ。
  • 2. 外部セッションストレージ(DynamoDB)の活用
    認証開始前に、クライアントが「認証ID」と「選択した方式」をペアで DynamoDB に書き込んでおく。Lambda 側でユーザーID等をキーにこれを参照し、フローを分岐させる。実際の運用現場では、この方法が最も確実だが、TTL(有効期限)の設定や書き込みコストの考慮が不可欠となる。
  • 3. ユーザー属性(Custom Attributes)による分岐
    「最後に利用した認証方式」をユーザー属性に保存しておき、それをデフォルトとして採用する。ただし、ユーザーが能動的に方式を切り替えたい場合には不向きだ。

実装コード例:ダミーチャレンジによる識別フロー

最も「Cognito ネイティブ」に近い解決策である、ダミーチャレンジを用いた DefineAuthChallenge のロジック例を以下に示す。


exports.handler = async (event) => {
    const { session } = event.request;
    const response = event.response;

    if (session.length === 0) {
        // 1回目:ClientMetadata が取れないため、一旦ダミーのチャレンジを出す
        response.issueTokens = false;
        response.failAuthentication = false;
        response.challengeName = 'CUSTOM_CHALLENGE';
        // ここでクライアントに「方式を教えてくれ」と促すパラメータを渡すことも可能
    } else {
        // 2回目以降:RespondToAuthChallenge 経由なので ClientMetadata が参照可能になる
        const authMethod = event.request.clientMetadata?.authMethod;

        if (authMethod === 'GOOGLE') {
            // Google ログイン用のロジックへ遷移
            handleGoogleFlow(event, response);
        } else {
            // デフォルトの OTP ロジックへ遷移
            handleOtpFlow(event, response);
        }
    }

    return event;
};

現場での注意点とハマりどころ

  • ステートレス性の維持: Lambda はステートレスであるべきだ。セッションを跨ぐ複雑な状態管理は、必ず Cognito の session 配列か外部 DB で完結させるべきだ。
  • セキュリティリスク: ダミーチャレンジを挟む場合、無意味なチャレンジ応答を繰り返す「リソース枯渇攻撃」への対策として、リトライ回数の制限を DefineAuthChallenge 内で厳密に定義せよ。
  • UX への影響: ダミーチャレンジを挟むことで、通信回数が1往復増える。わずかなレイテンシーだが、モバイルアプリなど低速な環境では無視できない差となる。

結論

Cognito Custom Auth において、初期リクエストのみで認証方式を完全に見分けるマジックは存在しない。「1往復多めにやり取りする(ダミーチャレンジ)」か「外部に状態を持たせる(DynamoDB)」かの二択が最適解となる。

Cognito の仕様に無理に抗うのではなく、これらの回避策を前提としたアーキテクチャを設計することが、堅牢な認証システム構築への最短ルートだ。

開発者は、プロジェクトの許容レイテンシーとコスト構造を比較し、最適なパターンを選択すべきだ。

タイトルとURLをコピーしました