はじめに

Amazon Cognito(以下Cognito)による認証では、IDトークンやアクセストークンに、認証時の状況やユーザーの選択に応じて変化する動的な情報を含めたい場合があります。

たとえば、アクセス元デバイスの種別や、ログイン時にユーザーが選択した操作対象の組織などが挙げられます。

氏名や所属組織などの固定情報はCognitoのユーザー属性として管理できますが、認証ごとに切り替わる情報はユーザー属性では扱うことができません。

動的な情報をトークンに含めるには、クライアントがClientMetadataでCognitoに情報を渡し、Cognitoがトークン生成前Lambdaを経由してその情報をトークンに追加する必要があります。

本記事では、この方法について、実装例を交えながら解説します。

想定読者

  • Cognitoを使用した認証(特にSRP認証、カスタム認証)についての基礎知識がある方
  • Cognitoが発行するトークンに動的な情報を埋め込みたい方

前提条件・環境情報

本記事のサンプルコードは、以下の環境で動作確認しています。

クライアント側(C#)

項目 バージョン
.NET 10.0
Amazon.Extensions.CognitoAuthentication 3.1.4
AWSSDK.CognitoIdentityProvider 4.0.6.2
System.IdentityModel.Tokens.Jwt 8.3.1

Lambda関数(JavaScript)

項目 設定値
ランタイム Node.js 24.x
アーキテクチャ x86_64

ClientMetadataとは

概要

ClientMetadataは、クライアントからCognitoのAPIを呼び出す際、任意の追加情報をキーと値のペアで指定できるパラメータです。

この情報は認証フローの中でLambdaトリガーに渡すことができ、トークンや認証処理のカスタマイズに利用できます。

var clientMetadata = new Dictionary<string, string>
{
    { "organizationId", "org-12345" },
    { "userRole", "admin" }
};

制約

ClientMetadataをトークン生成前Lambdaに渡すには、Cognitoの特定の認証APIを使用する必要があります。

ClientMetadataの受け渡しは、以下の2段階で行われます。

  • ステップ1:クライアントからCognitoへの送信
    どの認証APIでもClientMetadataをCognitoに送ることができます。
  • ステップ2:Cognitoからトークン生成前Lambdaへの送信
    トークン生成前LambdaにClientMetadataが伝わるかどうかは、ステップ1でどの認証APIで渡したかによって異なります。

ステップ1でInitiateAuth APIでClientMetadataを渡した場合、ステップ2でClientMetadataはトークン生成前Lambdaに届きません。

トークン生成前LambdaにClientMetadataを渡すには、RespondToAuthChallenge APIでClientMetadataを付与してCognitoを呼び出す必要があります。

(参考:Amazon Cognito の Lambda トリガーにClientMetadataを渡す | AWS re:Post

API トークン生成前Lambdaに届くかどうか
InitiateAuth API 届かない
RespondToAuthChallenge API 届く

※InitiateAuth APIでCognitoにClientMetadataを渡した場合、サインアップ前Lambda、認証前Lambda、ユーザー移行Lambdaに届けることができます。

※管理者権限で呼び出すAdmin API(AdminInitiateAuth API、AdminRespondToAuthChallenge API)もありますが、本記事ではユーザー権限で呼び出す非Admin API(InitiateAuth API、RespondToAuthChallenge API)を使用します。

パターン1:SRP認証(USER_SRP_AUTH)

概要

SRP(Secure Remote Password)認証は、Cognitoが標準で提供する安全な認証方式です。

SRP認証では必ずRespondToAuthChallenge APIが呼ばれるため、ClientMetadataをトークン生成前Lambdaに渡すことができます。

認証フロー開始時に呼び出すInitiateAuth APIでもClientMetadataを指定できますが、トークン生成前Lambdaには届きません。

トークン生成前Lambdaで利用できるのは、RespondToAuthChallenge APIで渡したClientMetadataのみです。

認証フロー

  1. InitiateAuth(USER_SRP_AUTH)
    クライアントがSRP_Aとユーザー名を送信してSRP認証を開始します。
    この時点でClientMetadataを渡すことができますが、トークン生成前Lambdaには届きません。
  2. PASSWORD_VERIFIERチャレンジ
    CognitoがSalt等を含んだチャレンジをクライアントに返却します。
  3. RespondToAuthChallenge(PASSWORD_VERIFIER)
    クライアントがパスワードと受け取った値から暗号学的証明(署名)を計算して送信します。
    この時点で渡したClientMetadataはトークン生成前Lambdaに届きます。
  4. Cognito認証判定
    Cognitoが送信された署名を検証し、認証の成否を判定します。
  5. トークン生成前Lambda呼び出し
    認証成功後、トークン発行前にトークン生成前Lambdaが呼び出されます。
    RespondToAuthChallengeで渡したClientMetadataはここで利用できます。
  6. トークン発行
    トークン生成前Lambdaで追加されたカスタムクレームを含むトークンが発行されます。

サンプル実装

トークン生成前Lambda(JavaScript)

AWS Lambdaコンソールで新しい関数を作成し、下記のコードをデプロイします。

/**
 * Pre Token Generation Lambda(V2.0)
 * 
 * ClientMetadataの内容をトークンのカスタムクレームに追加します。
 */
export const handler = async (event) => {
    console.log("Pre Token Generation Event:", JSON.stringify(event, null, 2));

    console.log("Trigger Source:", event.triggerSource);
    console.log("ClientId:", event.callerContext.clientId);

    // ClientMetadataを取得
    let clientMetadata = event.request.clientMetadata || {};
    console.log("Client Metadata:", JSON.stringify(clientMetadata, null, 2));

    // トリガーバージョンがV2.0の場合
    if (event.version === "2") {
        event.response = {
            claimsAndScopeOverrideDetails: {
                idTokenGeneration: {
                    claimsToAddOrOverride: {
                        "customKey1": clientMetadata.customKey1
                    }
                },
                accessTokenGeneration: {
                    claimsToAddOrOverride: {
                        "customKey1": clientMetadata.customKey1
                    }
                }
            }
        };
    }
    
    return event;
};

このLambda関数を、Cognito User Poolでトークン生成前トリガーとして設定します。

その際、トリガーイベントバージョンはV2.0を選択してください。V2.0では、IDトークンとアクセストークンに属性を追加できます。

動作確認

クライアント側(C#)

Amazon.Extensions.CognitoAuthentication SDKを使用すると、SRP認証の複雑な計算が自動化されるため、開発者は内部処理を意識することなく簡単に認証処理を実装できます。

StartWithSrpAuthAsyncでRespondToAuthChallengeを呼び出しています。

using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
using System.IdentityModel.Tokens.Jwt;
using Amazon.Extensions.CognitoAuthentication;
using Amazon.Runtime;

class Program
{
    static async Task Main()
    {
        var clientId = "XXXXXXXX"; // Cognito User PoolのApp Client IDを指定
        var userPoolId = "XXXXXXXX"; // Cognito User Pool IDを指定

        Console.WriteLine("=== Amazon Cognito デモ ===");

        await CustomSRPAuth(clientId, userPoolId);
    }


    static async Task CustomSRPAuth(string clientId, string userPoolId)
    {
        Console.Write("ユーザー名を入力してください: ");
        var userName = Console.ReadLine();

        Console.Write("パスワードを入力してください: ");
        var password = Console.ReadLine();

        // ClientMetadataを準備(トークンに埋め込みたい情報)
        var clientMetadata = new Dictionary<string, string>
        {
            { "customKey1", "customValue1" }
        };

        var provider = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), FallbackRegionFactory.GetRegionEndpoint());
        var userPool = new CognitoUserPool(userPoolId, clientId, provider);
        var user = new CognitoUser(userName, clientId, userPool, provider);

        var authRequest = new InitiateSrpAuthRequest
        {
            Password = password,
            ClientMetadata = clientMetadata  // トークンに埋め込みたい情報
        };
        
        // SDK内部で自動的にRespondToAuthChallenge APIが呼び出される
        AuthFlowResponse authResponse = await user.StartWithSrpAuthAsync(authRequest).ConfigureAwait(false);

        // 認証成功
        if (authResponse.AuthenticationResult != null)
        {
            Console.WriteLine("n認証成功!トークンを取得しました。");
            DisplayTokenInfo(authResponse.AuthenticationResult);
            return;
        }

        Console.WriteLine("n認証に失敗しました。");
    }

    static void DisplayTokenInfo(AuthenticationResultType authResult)
    {
        Console.WriteLine("n========== トークン情報 ==========");

        // IDトークンの情報を表示
        if (!string.IsNullOrEmpty(authResult.IdToken))
        {
            Console.WriteLine("n【IDトークンの属性情報】");
            var idTokenHandler = new JwtSecurityTokenHandler();
            var idToken = idTokenHandler.ReadJwtToken(authResult.IdToken);

            Console.WriteLine($"発行者 (Issuer): {idToken.Issuer}");
            Console.WriteLine($"対象者 (Audience): {string.Join(", ", idToken.Audiences)}");
            Console.WriteLine($"発行日時: {idToken.ValidFrom}");
            Console.WriteLine($"有効期限: {idToken.ValidTo}");
            Console.WriteLine($"nクレーム数: {idToken.Claims.Count()}");
            Console.WriteLine("nクレーム(属性情報):");
            foreach (var claim in idToken.Claims)
            {
                Console.WriteLine($"  {claim.Type}: {claim.Value}");
            }
        }

        // アクセストークンの情報を表示
        if (!string.IsNullOrEmpty(authResult.AccessToken))
        {
            Console.WriteLine("n【アクセストークンの属性情報】");
            var accessTokenHandler = new JwtSecurityTokenHandler();
            var accessToken = accessTokenHandler.ReadJwtToken(authResult.AccessToken);

            Console.WriteLine($"発行者 (Issuer): {accessToken.Issuer}");
            Console.WriteLine($"発行日時: {accessToken.ValidFrom}");
            Console.WriteLine($"有効期限: {accessToken.ValidTo}");
            Console.WriteLine($"nクレーム数: {accessToken.Claims.Count()}");
            Console.WriteLine("nクレーム(属性情報):");
            foreach (var claim in accessToken.Claims)
            {
                Console.WriteLine($"  {claim.Type}: {claim.Value}");
            }
        }

        // リフレッシュトークンの情報を表示(リフレッシュトークンはJWTではないため、存在確認のみ)
        if (!string.IsNullOrEmpty(authResult.RefreshToken))
        {
            Console.WriteLine("n【リフレッシュトークン】");
            Console.WriteLine($"リフレッシュトークンが発行されました(長さ: {authResult.RefreshToken.Length} 文字)");
        }

        Console.WriteLine("n===================================n");
    }
}

実行結果

発行されたIDトークン、アクセストークン共に、「customKey1: customValue1」という属性が追加されていることが分かります。

=== Amazon Cognito デモ ===
ユーザー名を入力してください: testuser
パスワードを入力してください: xxxxxxxx

認証成功!トークンを取得しました。

========== トークン情報 ==========

【IDトークンの属性情報】
発行者 (Issuer): https://cognito-idp.ap-northeast-1.amazonaws.com/xxxxxxxx
対象者 (Audience): xxxxxxxxxxxxxxxxxxxxxxxxxx
発行日時: 0001/01/01 0:00:00
有効期限: 2026/06/23 1:39:55

クレーム数: 11

クレーム(属性情報):
  origin_jti: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  sub: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  aud: xxxxxxxxxxxxxxxxxxxxxxxxxx
  token_use: id
  customKey1: customValue1                                       ←追加した属性
  auth_time: 1782175195
  iss: https://cognito-idp.ap-northeast-1.amazonaws.com/xxxxxxxx
  cognito:username: testuser
  exp: 1782178795
  iat: 1782175195
  jti: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

【アクセストークンの属性情報】
発行者 (Issuer): https://cognito-idp.ap-northeast-1.amazonaws.com/xxxxxxxx
発行日時: 0001/01/01 0:00:00
有効期限: 2026/06/23 1:39:55

クレーム数: 12

クレーム(属性情報):
  sub: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  iss: https://cognito-idp.ap-northeast-1.amazonaws.com/xxxxxxxx
  client_id: xxxxxxxxxxxxxxxxxxxxxxxxxx
  origin_jti: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  token_use: access
  customKey1: customValue1                                       ←追加した属性
  scope: aws.cognito.signin.user.admin
  auth_time: 1782175195
  exp: 1782178795
  iat: 1782175195
  jti: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  username: testuser

【リフレッシュトークン】
リフレッシュトークンが発行されました(長さ: 1664 文字)

===================================

パターン2:カスタム認証(CUSTOM_AUTH)

概要

カスタム認証フロー(CUSTOM_AUTH)でも、RespondToAuthChallenge APIでClientMetadataを渡すことで、トークンにカスタム情報を追加できます。

この方法は、OTP認証やパスワードレス認証など独自認証フローを実装する場合に有効です。

必要なLambda関数

カスタム認証フローを使ってClientMetadataで受け渡した情報をトークンに埋め込む場合、以下の4つのLambda関数が必要です。

  • カスタム認証用
    • 認証チャレンジ定義Lambda
    • 認証チャレンジ作成Lambda
    • 認証チャレンジレスポンス確認Lambda
  • トークンカスタマイズ用
    • トークン生成前Lambda(パターン1:SRP認証で作成したものと同一)

カスタム認証用のLambdaで認証が完了した後、トークンを発行する直前にトークン生成前Lambdaが呼び出されます。

RespondToAuthChallenge APIでClientMetadataを渡し、トークン生成前Lambdaでカスタムクレームを追加する流れはパターン1:SRP認証の場合と同じため、サンプル実装と動作確認は省略します。

なお、RespondToAuthChallenge APIで渡したClientMetadataは、認証チャレンジ定義Lambda、認証チャレンジ作成Lambda、認証チャレンジレスポンス確認Lambdaでも参照可能です。

一方、認証フロー開始時に呼び出すInitiateAuth APIで渡したClientMetadataは、これらのLambdaでは利用できません。

認証フロー

  1. InitiateAuth(CUSTOM_AUTH)
    クライアントがカスタム認証を開始します。
    この時点でClientMetadataを渡すことができますが、トークン生成前Lambdaには届きません。
  2. 認証チャレンジ定義Lambda(1回目)
    認証フローを定義するLambdaが呼ばれ、次に実行するチャレンジを決定します。
  3. 認証チャレンジ作成Lambda
    チャレンジを生成します。
  4. CUSTOM_CHALLENGEレスポンス
    Cognitoがチャレンジ内容をクライアントに返却します。
  5. RespondToAuthChallenge(CUSTOM_CHALLENGE)
    クライアントがチャレンジへの回答を送信します。
    この時点でClientMetadataを渡すと、トークン生成前Lambdaに届きます。
  6. 認証チャレンジレスポンス確認Lambda
    クライアントの回答を検証します。
  7. 認証チャレンジ定義Lambda(2回目)
    検証結果を受けて、認証完了かさらにチャレンジを続けるかを判定します。
  8. トークン生成前Lambda呼び出し
    認証完了後、トークン発行前にトークン生成前Lambdaが呼び出されます。
    RespondToAuthChallengeで渡したClientMetadataがここで利用可能になります。
  9. トークン発行
    トークン生成前Lambdaで追加されたカスタムクレームを含むトークンが発行されます。

参考:パスワード認証(USER_PASSWORD_AUTH)でClientMetadataが届かない理由

パスワード認証(USER_PASSWORD_AUTH)では、通常はInitiateAuth APIのみで認証が完了します。

この場合、RespondToAuthChallenge APIは呼ばれないため、ClientMetadataはトークン生成前Lambdaに届きません。

ただし、多要素認証(MFA)など認証チャレンジが発生した場合は、RespondToAuthChallenge APIが呼び出されるため、ClientMetadataをトークン生成前Lambdaに渡すことができます。

認証フロー

  1. InitiateAuth(USER_PASSWORD_AUTH)
    クライアントがユーザー名とパスワードを平文でCognitoに送信します。
    ClientMetadataも渡すことができますが、トークン生成前Lambdaには届きません。
  2. Cognito認証判定
    Cognitoがユーザー名とパスワードを検証します。
  3. トークン生成前Lambda呼び出し
    認証成功後、トークン発行前にトークン生成前Lambdaが呼び出されます。
    しかし、InitiateAuthで渡したClientMetadataはここでは利用できません。
  4. トークン発行
    カスタムクレームを追加できないままトークンが発行されます。

 

おわりに

本記事では、ClientMetadataをRespondToAuthChallenge APIでCognitoに渡し、トークン生成前Lambdaを使うことで、動的な情報をトークンに埋め込む方法を説明しました。

参考資料