Streamlit × Cognito でカスタム OTP ログインを実装する

Streamlit × Cognito でカスタム OTP ログインを実装する

2025.12.24

はじめに

こんにちは、コンサルティング部のぐっさんです。寒いですね。
本記事では、CDK を使って Cognito のカスタム認証用Lambdaを設定し、認証に「ワンタイムパスワード(OTP)」を使用する簡単なデモアプリケーションの実装を試します。
フロントエンドに Streamlit、ホスティングに ECS/Fargate + ALB を使用し、開発言語はすべて Python で統一した構成にしてみました。

作るもの

こんな画面のイメージです。

IMG

  • ユーザー登録(メール + パスワード)
  • ログイン時にパスワード認証後、メールで OTP を送信
  • OTP を入力すればログイン完了

なぜ Cognito 標準の MFA ではなく、カスタム OTP?

Cognito には標準で MFA 機能がありますが、カスタム認証を使うことで以下のようなカスタマイズや仕組み理解が出来るためです。

  • メール本文を自由にカスタマイズ可能
  • OTP の有効期限、試行回数制限などを柔軟に設定可能
  • 認証フローの仕組みを理解できる(学習目的)

構成図

簡単な構成図です。
一部省略していますが、ECSとCognito間の通信はNAT Gateway経由にしています。

ar


前提条件

  • Python 3.12+
  • Node.js 18+ (CDK CLI 用)
  • AWS CLI 設定済み
  • Docker
  • SES で検証済みのメールアドレス(サンドボックスモードの場合)

プロジェクト構成

cdk-streamlit-cognito/
├── app.py                 # CDK エントリポイント
├── cdk.json
├── requirements.txt       # CDK用
│
├── stacks/
│   └── main_stack.py      # CDK スタック定義
│
├── lambda/                # カスタム認証 Lambda群
│   ├── define_auth/
│   │   └── index.py
│   ├── create_auth/
│   │   └── index.py
│   └── verify_auth/
│       └── index.py
│
└── streamlit/
    ├── app.py             # Streamlit アプリ
    ├── Dockerfile
    └── requirements.txt

Cognito カスタム認証の仕組み

Cognito のカスタム認証チャレンジは、3つの Lambda トリガーで構成されます。

認証フロー

  1. ログイン開始 - ユーザーがメールアドレスとパスワードを入力
  2. パスワード検証 - Cognito が SRP プロトコルでパスワードを検証
  3. OTP 送信 - パスワードが正しければ、Lambda が OTP を生成してメール送信
  4. OTP 入力 - ユーザーが届いた 6 桁のコードを入力
  5. OTP 検証 - Lambda が OTP を検証、正しければトークン発行
[ユーザー] ──(1)メール+パスワード──> [Cognito] ──(2)パスワード検証
                                        │
                                        ▼
[ユーザー] <──(3)OTPメール────────── [Lambda] ──> [SES]
                                        │
[ユーザー] ──(4)OTP入力──────────────> [Cognito]
                                        │
                                        ▼
[ユーザー] <──(5)トークン発行──────── [Lambda] ──(OTP検証)

SRP(Secure Remote Password)とは?

SRP はパスワードをネットワークに送信せずに認証できる暗号プロトコルです。
クライアントとサーバーが数学的な計算を交換することで、パスワードが正しいかを検証します。

Cognito は標準で SRP をサポートしており、今回は pycognito ライブラリがこの処理を担当します。

私は数学が得意ではないので、プロトコルについてのイメージをだいぶ簡易的に図解してみました。
以下、よかったら参考までに!

https://dev.classmethod.jp/articles/cognito-srp/

3つの Lambda の役割

Lambda 役割
DefineAuthChallenge 認証フローの制御(次に何をするか決定)
CreateAuthChallenge OTP を生成して SES でメール送信
VerifyAuthChallenge ユーザーが入力した OTP が正しいか検証

CDK スタック実装

プロジェクト初期化

mkdir cdk-streamlit-cognito
cd cdk-streamlit-cognito

# 仮想環境作成 & アクティベート
python -m venv .venv
source .venv/bin/activate

# 依存パッケージインストール
pip install -r requirements.txt

requirements.txt (CDK用)

aws-cdk-lib>=2.170.0
constructs>=10.0.0

stacks/main_stack.py

from aws_cdk import (
    Stack,
    CfnOutput,
    aws_cognito as cognito,
    aws_lambda as lambda_,
    aws_ec2 as ec2,
    aws_ecs as ecs,
    aws_ecs_patterns as ecs_patterns,
    aws_ecr_assets as ecr_assets,
    aws_iam as iam,
    Duration,
    RemovalPolicy,
)
from constructs import Construct

class MainStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ===================
        # Lambda Functions
        # ===================
        define_auth_fn = lambda_.Function(
            self,
            "DefineAuthChallenge",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda/define_auth"),
            timeout=Duration.seconds(30),
        )

        create_auth_fn = lambda_.Function(
            self,
            "CreateAuthChallenge",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda/create_auth"),
            timeout=Duration.seconds(30),
        )

        verify_auth_fn = lambda_.Function(
            self,
            "VerifyAuthChallenge",
            runtime=lambda_.Runtime.PYTHON_3_12,
            handler="index.handler",
            code=lambda_.Code.from_asset("lambda/verify_auth"),
            timeout=Duration.seconds(30),
        )

        # ===================
        # Cognito User Pool
        # ===================
        user_pool = cognito.UserPool(
            self,
            "UserPool",
            user_pool_name="custom-otp-pool",
            self_sign_up_enabled=True,
            sign_in_aliases=cognito.SignInAliases(email=True),
            auto_verify=cognito.AutoVerifiedAttrs(email=True),
            # Lambda トリガー
            lambda_triggers=cognito.UserPoolTriggers(
                define_auth_challenge=define_auth_fn,
                create_auth_challenge=create_auth_fn,
                verify_auth_challenge_response=verify_auth_fn,
            ),
            removal_policy=RemovalPolicy.DESTROY,
        )

        # CreateAuthChallenge Lambda に AdminGetUser と SES 権限を付与
        create_auth_fn.add_to_role_policy(
            iam.PolicyStatement(
                actions=["cognito-idp:AdminGetUser"],
                resources=[
                    f"arn:aws:cognito-idp:{self.region}:{self.account}:userpool/*"
                ],
            )
        )
        create_auth_fn.add_to_role_policy(
            iam.PolicyStatement(
                actions=["ses:SendEmail"],
                resources=["*"],
            )
        )

        # User Pool Client
        user_pool_client = user_pool.add_client(
            "AppClient",
            auth_flows=cognito.AuthFlow(
                custom=True,  # カスタム認証を有効化
                user_srp=True,  # SRP認証も有効化
            ),
            generate_secret=False,
        )

        # ===================
        # VPC
        # ===================
        vpc = ec2.Vpc(
            self,
            "Vpc",
            max_azs=2,
            nat_gateways=1,
        )

        # ===================
        # ECS Cluster
        # ===================
        cluster = ecs.Cluster(
            self,
            "Cluster",
            vpc=vpc,
        )

        # ===================
        # Docker Image
        # ===================
        image_asset = ecr_assets.DockerImageAsset(
            self,
            "StreamlitImage",
            directory="./streamlit",
        )

        # ===================
        # Fargate Service with ALB
        # ===================
        fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService(
            self,
            "StreamlitService",
            cluster=cluster,
            cpu=256,
            memory_limit_mib=512,
            desired_count=1,
            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
                image=ecs.ContainerImage.from_docker_image_asset(image_asset),
                container_port=8501,
                environment={
                    "USER_POOL_ID": user_pool.user_pool_id,
                    "CLIENT_ID": user_pool_client.user_pool_client_id,
                    "AWS_REGION": self.region,
                },
            ),
            public_load_balancer=True,
        )

        # Cognito へのアクセス権限
        fargate_service.task_definition.task_role.add_to_policy(
            iam.PolicyStatement(
                actions=[
                    "cognito-idp:InitiateAuth",
                    "cognito-idp:RespondToAuthChallenge",
                    "cognito-idp:SignUp",
                    "cognito-idp:ConfirmSignUp",
                ],
                resources=[
                    f"arn:aws:cognito-idp:{self.region}:{self.account}:userpool/*"
                ],
            )
        )

        # ヘルスチェック設定
        fargate_service.target_group.configure_health_check(
            path="/_stcore/health",
            healthy_http_codes="200",
        )

        # ===================
        # Outputs
        # ===================
        CfnOutput(
            self,
            "UserPoolId",
            value=user_pool.user_pool_id,
            description="Cognito User Pool ID",
        )

        CfnOutput(
            self,
            "UserPoolClientId",
            value=user_pool_client.user_pool_client_id,
            description="Cognito User Pool Client ID",
        )

        CfnOutput(
            self,
            "ServiceUrl",
            value=f"http://{fargate_service.load_balancer.load_balancer_dns_name}",
            description="ALB URL",
        )

Lambda 実装

lambda/define_auth/index.py

認証フローを制御する Lambda です。セッションの状態に応じて次のチャレンジを決定します。

import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    """
    認証フローの次のステップを決定する
    """
    username = event.get("userName", "unknown")
    session = event["request"]["session"]
    session_length = len(session)

    logger.info(f"User: {username}, Session step: {session_length}")

    # 空セッション → PASSWORD_VERIFIER を開始
    if session_length == 0:
        challenge = "PASSWORD_VERIFIER"
        event["response"]["challengeName"] = challenge
        event["response"]["issueTokens"] = False
        event["response"]["failAuthentication"] = False
        logger.info(f"Next challenge: {challenge}")

    # PASSWORD_VERIFIER 完了後 → OTP チャレンジ
    elif (
        session_length == 1
        and session[-1]["challengeName"] == "PASSWORD_VERIFIER"
        and session[-1]["challengeResult"]
    ):
        challenge = "CUSTOM_CHALLENGE"
        event["response"]["challengeName"] = challenge
        event["response"]["issueTokens"] = False
        event["response"]["failAuthentication"] = False
        logger.info(f"Password verified. Next challenge: {challenge}")

    # OTP 正解 → トークン発行
    elif (
        session_length == 2
        and session[-1]["challengeName"] == "CUSTOM_CHALLENGE"
        and session[-1]["challengeResult"]
    ):
        event["response"]["issueTokens"] = True
        event["response"]["failAuthentication"] = False
        logger.info("OTP verified. Issuing tokens.")

    # 失敗
    else:
        event["response"]["issueTokens"] = False
        event["response"]["failAuthentication"] = True
        logger.warning(f"Authentication failed. Session: {session}")

    return event

lambda/create_auth/index.py

OTP を生成して SES でメール送信する Lambda です。

import logging
import random
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ses_client = boto3.client("ses")
cognito_client = boto3.client("cognito-idp")

def handler(event, context):
    """
    OTP チャレンジを作成し、メールで送信する
    """
    user_pool_id = event["userPoolId"]
    username = event["userName"]

    # ユーザーのメールアドレスを取得
    user = cognito_client.admin_get_user(UserPoolId=user_pool_id, Username=username)
    email = ""
    for attr in user["UserAttributes"]:
        if attr["Name"] == "email":
            email = attr["Value"]
            break

    # 6桁のOTPを生成
    otp = str(random.randint(100000, 999999))

    logger.info(f"User: {username}, Email: {email[:3]}***")

    # メール送信
    if email:
        try:
            ses_client.send_email(
                Source=email,  # SESで検証済みのメールアドレス
                Destination={"ToAddresses": [email]},
                Message={
                    "Subject": {"Data": "ログイン認証コード", "Charset": "UTF-8"},
                    "Body": {
                        "Text": {
                            "Data": f"あなたの認証コードは {otp} です。\n\n有効期限は3分間です。",
                            "Charset": "UTF-8",
                        }
                    },
                },
            )
            logger.info("OTP email sent successfully")
        except Exception as e:
            logger.error(f"Failed to send OTP email: {type(e).__name__}: {str(e)}")

    # 公開パラメータ(クライアントに送る)
    event["response"]["publicChallengeParameters"] = {
        "email": email[:3] + "***" + email[email.index("@"):] if email else ""
    }

    # 非公開パラメータ(検証用、クライアントには送らない)
    event["response"]["privateChallengeParameters"] = {"otp": otp}

    event["response"]["challengeMetadata"] = "OTP_CHALLENGE"

    return event

lambda/verify_auth/index.py

ユーザーが入力した OTP を検証する Lambda です。

import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    """
    OTP を検証する
    """
    username = event.get("userName", "unknown")
    expected_otp = event["request"]["privateChallengeParameters"]["otp"]
    user_otp = event["request"]["challengeAnswer"]

    # OTP が一致するか確認
    if user_otp == expected_otp:
        event["response"]["answerCorrect"] = True
        logger.info(f"User: {username}, OTP verification: SUCCESS")
    else:
        event["response"]["answerCorrect"] = False
        logger.warning(f"User: {username}, OTP verification: FAILED")

    return event

Streamlit 実装

streamlit/requirements.txt

streamlit>=1.40.0
boto3>=1.35.0
pycognito>=2024.5.1

streamlit/app.py

import os

import boto3
import streamlit as st
from botocore.exceptions import ClientError
from pycognito import AWSSRP

# 環境変数から取得
USER_POOL_ID = os.environ.get("USER_POOL_ID", "")
CLIENT_ID = os.environ.get("CLIENT_ID", "")
REGION = os.environ.get("AWS_REGION", "ap-northeast-1")

# Cognito クライアント
cognito_client = boto3.client("cognito-idp", region_name=REGION)

def main():
    st.title("カスタム OTP 認証デモ")

    # セッション状態の初期化
    if "authenticated" not in st.session_state:
        st.session_state.authenticated = False
    if "auth_session" not in st.session_state:
        st.session_state.auth_session = None
    if "challenge" not in st.session_state:
        st.session_state.challenge = None
    if "username" not in st.session_state:
        st.session_state.username = None
    if "confirmation_required" not in st.session_state:
        st.session_state.confirmation_required = False

    # 認証済みの場合
    if st.session_state.authenticated:
        show_dashboard()
    # メール確認待ち
    elif st.session_state.confirmation_required:
        show_confirmation()
    # OTP チャレンジ中
    elif st.session_state.challenge:
        show_challenge()
    # 未認証の場合
    else:
        show_login()

def show_login():
    """ログイン画面"""
    tab1, tab2 = st.tabs(["ログイン", "新規登録"])

    with tab1:
        st.subheader("ログイン")
        email = st.text_input("メールアドレス", key="login_email")
        password = st.text_input("パスワード", type="password", key="login_password")

        if st.button("ログイン"):
            if not email or not password:
                st.error("メールアドレスとパスワードを入力してください")
                return

            try:
                # SRP 認証を使用してカスタムチャレンジを取得
                aws_srp = AWSSRP(
                    username=email,
                    password=password,
                    pool_id=USER_POOL_ID,
                    client_id=CLIENT_ID,
                    client=cognito_client,
                )

                # CUSTOM_AUTH フローで SRP 認証を実行
                auth_params = aws_srp.get_auth_params()

                # Step 1: CUSTOM_AUTH 開始 → PASSWORD_VERIFIER が返ってくる
                response = cognito_client.initiate_auth(
                    ClientId=CLIENT_ID,
                    AuthFlow="CUSTOM_AUTH",
                    AuthParameters=auth_params,
                )

                # Step 2: PASSWORD_VERIFIER に応答 → CUSTOM_CHALLENGE が返ってくる
                if response.get("ChallengeName") == "PASSWORD_VERIFIER":
                    challenge_response = aws_srp.process_challenge(
                        response["ChallengeParameters"], auth_params
                    )
                    response = cognito_client.respond_to_auth_challenge(
                        ClientId=CLIENT_ID,
                        ChallengeName="PASSWORD_VERIFIER",
                        Session=response["Session"],
                        ChallengeResponses=challenge_response,
                    )
                else:
                    st.warning(f"予期しないチャレンジ: {response.get('ChallengeName')}")
                    return

                # カスタムチャレンジが返ってきた場合
                if response.get("ChallengeName") == "CUSTOM_CHALLENGE":
                    st.session_state.challenge = response
                    st.session_state.auth_session = response["Session"]
                    st.session_state.username = email
                    st.rerun()
                # 認証成功(チャレンジなし)
                elif "AuthenticationResult" in response:
                    st.session_state.authenticated = True
                    st.session_state.tokens = response["AuthenticationResult"]
                    st.rerun()
                else:
                    st.warning(f"予期しないレスポンス: {response.get('ChallengeName')}")

            except ClientError as e:
                st.error(f"ログインエラー: {e.response['Error']['Message']}")
            except Exception as e:
                st.error(f"エラー: {type(e).__name__}: {str(e)}")

    with tab2:
        st.subheader("新規登録")
        new_email = st.text_input("メールアドレス", key="signup_email")
        new_password = st.text_input(
            "パスワード (8文字以上、大文字・小文字・数字・記号含む)", type="password", key="signup_password"
        )

        if st.button("登録"):
            if not new_email or not new_password:
                st.error("メールアドレスとパスワードを入力してください")
                return

            try:
                # ユーザー登録
                response = cognito_client.sign_up(
                    ClientId=CLIENT_ID,
                    Username=new_email,
                    Password=new_password,
                    UserAttributes=[
                        {"Name": "email", "Value": new_email},
                    ],
                )

                st.session_state.username = new_email
                st.session_state.confirmation_required = True
                st.success("登録メールを送信しました。確認コードを入力してください。")
                st.rerun()

            except ClientError as e:
                st.error(f"登録エラー: {e.response['Error']['Message']}")

def show_confirmation():
    """メール確認画面"""
    st.subheader("メール確認")
    st.write(f"**{st.session_state.username}** に確認コードを送信しました。")

    code = st.text_input("確認コード")

    if st.button("確認"):
        try:
            cognito_client.confirm_sign_up(
                ClientId=CLIENT_ID,
                Username=st.session_state.username,
                ConfirmationCode=code,
            )

            st.success("登録完了!ログインしてください。")
            st.session_state.confirmation_required = False
            st.rerun()

        except ClientError as e:
            st.error(f"確認エラー: {e.response['Error']['Message']}")

    if st.button("確認コードを再送信"):
        try:
            cognito_client.resend_confirmation_code(
                ClientId=CLIENT_ID, Username=st.session_state.username
            )
            st.success("確認コードを再送信しました")
        except ClientError as e:
            st.error(f"再送信エラー: {e.response['Error']['Message']}")

def show_challenge():
    """OTP チャレンジ画面"""
    st.subheader("認証コード入力")

    challenge = st.session_state.challenge
    masked_email = challenge.get("ChallengeParameters", {}).get("email", "")

    st.info(f"**{masked_email}** に認証コードを送信しました。")
    otp = st.text_input("6桁の認証コードを入力してください", max_chars=6)

    col1, col2 = st.columns(2)

    with col1:
        if st.button("認証"):
            try:
                response = cognito_client.respond_to_auth_challenge(
                    ClientId=CLIENT_ID,
                    ChallengeName="CUSTOM_CHALLENGE",
                    Session=st.session_state.auth_session,
                    ChallengeResponses={
                        "USERNAME": st.session_state.username,
                        "ANSWER": otp,
                    },
                )

                # 認証成功
                if "AuthenticationResult" in response:
                    st.session_state.authenticated = True
                    st.session_state.tokens = response["AuthenticationResult"]
                    st.session_state.challenge = None
                    st.rerun()
                # さらにチャレンジがある場合
                elif "ChallengeName" in response:
                    st.session_state.challenge = response
                    st.session_state.auth_session = response["Session"]
                    st.rerun()

            except ClientError as e:
                st.error(f"認証エラー: {e.response['Error']['Message']}")

    with col2:
        if st.button("キャンセル"):
            st.session_state.challenge = None
            st.session_state.auth_session = None
            st.session_state.username = None
            st.rerun()

def show_dashboard():
    """認証後のダッシュボード"""
    # ヘッダー
    col1, col2 = st.columns([3, 1])
    with col1:
        st.title("ダッシュボード")
    with col2:
        if st.button("ログアウト", type="secondary"):
            st.session_state.authenticated = False
            st.session_state.tokens = None
            st.session_state.challenge = None
            st.session_state.auth_session = None
            st.session_state.username = None
            st.rerun()

    st.divider()

    # ユーザー情報カード
    st.subheader("ユーザー情報")
    col1, col2 = st.columns(2)
    with col1:
        st.metric(label="ログイン中のユーザー", value=st.session_state.username or "不明")
    with col2:
        st.metric(label="認証方式", value="OTP (Email)")

    st.divider()

    # 認証成功メッセージ
    st.success("カスタム OTP による多要素認証が完了しました!")

    # 機能説明
    st.subheader("このデモについて")
    st.markdown("""
    このアプリは **Amazon Cognito** のカスタム認証フローを使用して、
    以下の多要素認証を実装しています:

    1. **パスワード認証** - SRP プロトコルによる安全なパスワード検証
    2. **OTP 認証** - メールで送信される6桁のワンタイムパスワード

    すべて **Python** で構築されています:
    - インフラ: AWS CDK (Python)
    - バックエンド: AWS Lambda (Python)
    - フロントエンド: Streamlit (Python)
    """)

    # トークン情報の表示(デバッグ用)
    with st.expander("トークン情報(デバッグ用)"):
        if "tokens" in st.session_state:
            st.json(
                {
                    "AccessToken": st.session_state.tokens.get("AccessToken", "")[:50]
                    + "...",
                    "IdToken": st.session_state.tokens.get("IdToken", "")[:50] + "...",
                    "ExpiresIn": st.session_state.tokens.get("ExpiresIn"),
                    "TokenType": st.session_state.tokens.get("TokenType"),
                }
            )

if __name__ == "__main__":
    main()

streamlit/Dockerfile

FROM --platform=linux/amd64 python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8501

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0", "--server.headless=true", "--server.enableCORS=false", "--server.enableXsrfProtection=false", "--browser.gatherUsageStats=false"]

デプロイ

# 仮想環境アクティベート
source .venv/bin/activate

# ブートストラップ(初回のみ)
cdk bootstrap

# デプロイ
cdk deploy

動作確認

  1. デプロイ後に出力される ALB の URL にアクセス

cfn_out

  1. 新規登録でメールアドレスとパスワードを入力

stream_1

  1. Cognito から届いた確認コードを入力(サインアップ確認)

stream_2

stream_3

stream_4

  1. この時点で新規ユーザーがCognitoに登録されたことを確認

cognito_3

  1. ログインでメールアドレスとパスワードを入力

stream_5

  1. SES から OTP がメールで届く(※SESがサンドボックス環境の場合、迷惑メールフォルダに入る可能性が高いです)

stream_6

stream_7

  1. OTP を入力

stream_9

  1. 以下のようなダッシュボードが表示されればOTPを使用したカスタム認証成功!

stream_8


お片付け

CDKでデプロイしたCFnをコンソールから削除するか、以下のcdkコマンドで環境を削除します。

cdk destroy

ハマった部分など

1. App Runner は WebSocket 非対応

当初 App Runner でフロント構築しようとしましたが、Streamlit は WebSocket を使用するため動作しませんでした。。。
ECS/Fargate + ALB であれば WebSocket が正常に動作します。

このissueに辿り着いて泣く泣く諦めました。サポートしてもらえたらとても嬉しいです・・・!
https://github.com/aws/apprunner-roadmap/issues/13

2. SES サンドボックス

SES はデフォルトでサンドボックスモードです。
送信元・送信先ともに検証済みメールアドレスが必要です。

aws ses verify-email-identity --email-address your-email@example.com

また、サンドボックスモードではメールが迷惑メールフォルダに振り分けられやすいです。
私も最初、迷惑メールフォルダに届いて少々時間を取られました。
OTP メールが届かない場合は、迷惑メールフォルダを確認してみてください。

3. カスタム認証フローの有効化

User Pool Client で auth_flows.custom=True を忘れずに設定しましょう。

4. DefineAuthChallenge の役割

DefineAuthChallenge Lambda は認証の各ステップで呼ばれ、「次に何をするか」を決定します。
セッション(session)に過去のチャレンジ結果が蓄積されていくので、その長さと結果を見て判断します。

セッション状態 返すチャレンジ 意味
空(初回) PASSWORD_VERIFIER パスワード検証を開始
パスワード検証成功後 CUSTOM_CHALLENGE OTP チャレンジを発行
OTP 検証成功後 issueTokens = True トークン発行(認証完了)

実装時、セッションが空で返ってきてエラーになることが多かったです。
デバッグでちょこちょこレスポンスの中身を見ながら進めると良さそうでした。

5. Docker イメージのプラットフォーム

Apple Silicon Mac でビルドする場合、--platform=linux/amd64 を指定しないと
Fargate で実行エラーになりますのでご注意。


まとめ

Cognito のカスタム認証チャレンジで「ワンタイムパスワード(OTP)」を実装してみました。
OTP送信時のメール文面(Body)にLambda側で好きなメッセージを入れられるため、ある程度カスタムできて面白かったです。
認証に役割違いのLambdaが3つ必要な点も勉強になりました。
アプリケーションにオリジナルの認証を付与したい時はぜひ試してみてください!
最後までお読み頂きありがとうございました。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事