TypeScript で実装する Amazon Cognito User Pools 認証システム【MFA 対応・Terraform 構築】
業務で Cognito を使用した認証システムの実装を担当することになり、AWS Cognito をはじめて使用したため、ドキュメントを参考にしながら学んだ内容をまとめました。
項目 | バージョン |
---|---|
Mac | Ventura 15.5 |
Terraform | 1.12.1 |
Node.js | 24.1.0 |
パッケージのインストール
Section titled “パッケージのインストール”pnpm init
pnpm add @aws-sdk/client-cognito-identity-provider
pnpm add -D tsx typescript @types/node
# tsconfig.json作成pnpm tsc --init
Cognito User Pools の構築
Section titled “Cognito User Pools の構築”Terraform でインフラを作成する
Section titled “Terraform でインフラを作成する”作成手順:
-
リソース定義ファイルを作成
- コード全体は terraform/cognito.tf を参照
-
インフラを構築
Terminal window cd terraformterraform initterraform planterraform apply
主要な設定項目:
resource "aws_cognito_user_pool" "main" { # ...省略
# NOTE: alias_attributesを設定している場合、username属性は設定できない # username_attributes = []
alias_attributes = ["email"] auto_verified_attributes = ["email", "phone_number"]
mfa_configuration = "OPTIONAL"
# ...省略}
重要な設定項目
Section titled “重要な設定項目”エイリアス属性:
alias_attributes
:ユーザーがログインに使用できる属性- 今回の設定:メールアドレス
自動確認属性:
auto_verified_attributes
:サインアップ時に自動確認する属性- 今回の設定:メールアドレスと電話番号
エイリアス属性を使用する理由
Section titled “エイリアス属性を使用する理由”username 属性の問題点:
- ユーザー作成後に変更不可
- メールアドレス変更に対応できない
エイリアス属性の利点:
- メールアドレスでログイン可能
- サインアップ後にメールアドレス変更可能
- 運用の柔軟性が向上
参考:ユーザー属性の操作 - AWS Documentation
ユーザー登録フローの実装
Section titled “ユーザー登録フローの実装”1. 新規ユーザーを作成する(SignUpCommand)
Section titled “1. 新規ユーザーを作成する(SignUpCommand)”機能:メールアドレスを使用してユーザーを新規作成
async function signUpUser() { const command = new SignUpCommand({ ClientId: CLIENT_ID, Username: MY_USERNAME_A, Password: MY_PASSWORD, UserAttributes: [ { Name: "email", Value: MY_EMAIL, }, ], });
const response = await client.send(command); console.log(response);}
実行結果:
$ pnpm tsx src/signup/signup-command.ts{ '$metadata': { httpStatusCode: 200, requestId: 'a7a3cca9-b8a0-46e5-beaf-c22941d51a69', // ...省略 }, CodeDeliveryDetails: { AttributeName: 'email', DeliveryMedium: 'EMAIL', Destination: '****@****' }, UserConfirmed: false, UserSub: '27f45a88-9071-705a-a290-e9f5361c9226'}
処理結果:
- 確認ステータス:未確認
- メールに確認コードが送信される
2. サインアップを確認する(ConfirmSignUpCommand)
Section titled “2. サインアップを確認する(ConfirmSignUpCommand)”機能:メールで受信した確認コードを使用してサインアップを確認
async function confirmSignUpUser() { const command = new ConfirmSignUpCommand({ ClientId: CLIENT_ID, Username: MY_USERNAME_A, ConfirmationCode: confirmationCode, ForceAliasCreation: true, });
const response = await client.send(command); console.log(response);}
実行結果:
$ pnpm tsx src/signup/confirm-signup-command.ts 155675{ '$metadata': { httpStatusCode: 200, requestId: '1d32ca34-e04f-4cbd-803f-5ff5288ecdeb', // ...省略 }}
確認後の状態:
- E メール確認済み:はい
- 確認ステータス:確認済み
3. MFA を有効化する
Section titled “3. MFA を有効化する”ユーザープールの MFA 設定:オプション
MFA を強制していないため、ユーザーごとに MFA を有効化できます。
MFA 有効化の実装:
// ...省略async function setUserMFAPreference() { const command = new AdminSetUserMFAPreferenceCommand({ UserPoolId: USER_POOL_ID, Username: MY_EMAIL, // ユーザー名としてメールアドレスを使用 // SMSMfaSettings: { // Enabled: true, // PreferredMfa: false, // }, // SoftwareTokenMfaSettings: { // Enabled: false, // PreferredMfa: false, // }, EmailMfaSettings: { Enabled: true, PreferredMfa: true, }, });
const response = await client.send(command); console.log(response);}
実行結果:
$ pnpm tsx src/signup/set-user-mfa-preference-command.ts{ '$metadata': { httpStatusCode: 200, requestId: '7d17f07b-ebe6-4727-b1e7-683ebac3e253', // ...省略 }}
MFA 有効化の実装方針
Section titled “MFA 有効化の実装方針”目標:
- アプリケーションで MFA を強制する
- サインアップ後にホーム画面へスムーズに遷移
- セッションにアクセストークンを保持
課題:
ConfirmSignUpCommand
実行後、アクセストークン取得にはログイン処理が必要です。これによりユーザーはサインアップ後に再度ログインが必要になり、ユーザビリティが低下します。
解決策:
サーバー側で以下の処理を順次実行し、ユーザーの手間を省きます:
ConfirmSignUpCommand
を実行InitiateAuthCommand
やAdminInitiateAuthCommand
を実行(アクセストークン取得)- ユーザープールで MFA を強制した場合、MFA コード入力が必要
SetUserMFAPreferenceCommand
を実行(MFA 有効化)
効果:サインアップ後に直接ホーム画面に遷移可能
管理者機能と重複メールアドレスの処理
Section titled “管理者機能と重複メールアドレスの処理”エイリアス属性の制限事項
Section titled “エイリアス属性の制限事項”ConfirmSignUpCommand では、alias 属性に設定したメールアドレスが既存の場合、AliasExistsException
が発生します。
解決方法:
AliasExistsException
でエラーハンドリングForceAliasCreation
オプションを使用して alias 作成を強制
AdminConfirmSignUpCommand では、管理者権限でユーザー確認を強制できます。
実際の動作確認
Section titled “実際の動作確認”テスト条件:同じメールアドレスを持つ別のユーザーを作成
// ...省略
async function signUpUser() { const command = new SignUpCommand({ ClientId: CLIENT_ID, Username: MY_USERNAME_A, Username: MY_USERNAME_B, Password: MY_PASSWORD, UserAttributes: [ { Name: "email", Value: MY_EMAIL, }, ], }); // ...省略}
1. ForceAliasCreation なしの場合
// ...省略
async function confirmSignUpUser() { const command = new ConfirmSignUpCommand({ ClientId: CLIENT_ID, Username: MY_USERNAME_A, Username: MY_USERNAME_B, ConfirmationCode: confirmationCode, }); // ...省略}
結果:AliasExistsException
が発生
$ pnpm tsx src/signup/confirm-signup-command.ts 827653AliasExistsException: An account with the email already exists. at de_AliasExistsExceptionRes (/path/to/node_modules/@aws-sdk/client-cognito-identity-provider/dist-cjs/index.js:4585:21) at de_CommandError (/path/to/node_modules/@aws-sdk/client-cognito-identity-provider/dist-cjs/index.js:4470:19) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async /path/to/node_modules/@smithy/middleware-serde/dist-cjs/index.js:36:20 at async /path/to/node_modules/@smithy/core/dist-cjs/index.js:193:18 at async /path/to/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38 at async /path/to/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:33:22 at async confirmSignUpUser (/path/to/src/signup/confirm-signup-command.ts:20:20){ '$fault': 'client', '$metadata': { httpStatusCode: 400, requestId: '5916e84b-13ff-45b4-bc47-0925e3dbd6d6', extendedRequestId: undefined, cfId: undefined, attempts: 1, totalRetryDelay: 0 }, __type: 'AliasExistsException'}
2. ForceAliasCreation: true の場合
ユーザーを再作成後、ForceAliasCreation
オプションを true
にして ConfirmSignUpCommand
を実行します。
// ...省略
async function confirmSignUpUser() { const command = new ConfirmSignUpCommand({ ClientId: CLIENT_ID, Username: MY_USERNAME_A, Username: MY_USERNAME_B, ConfirmationCode: confirmationCode, ForceAliasCreation: true, }); // ...省略}
結果:
- E メール確認済み:はい
- 確認ステータス:確認済み
3. AdminConfirmSignUpCommand の場合
ユーザーを再作成後、AdminConfirmSignUpCommand
を実行します。
async function adminConfirmSignUp() { const command = new AdminConfirmSignUpCommand({ UserPoolId: USER_POOL_ID, Username: MY_USERNAME_B });
const response = await client.send(command); console.log(response);}
$ pnpm tsx src/signup/admin-confirm-signup-command.ts{ '$metadata': { httpStatusCode: 200, requestId: '2f660d4d-da03-4388-87ad-a7638b7e1ba7', // ...省略 }}
結果:
- E メール確認済み:いいえ
- 確認ステータス:確認済み
各コマンドの動作まとめ
Section titled “各コマンドの動作まとめ”認証フローの実装
Section titled “認証フローの実装”1. ログインを開始する(InitiateAuthCommand)
Section titled “1. ログインを開始する(InitiateAuthCommand)”機能:ユーザー名とパスワードを使用してログインを開始
// ...省略async function initiateAuth() { const command = new InitiateAuthCommand({ AuthFlow: "USER_PASSWORD_AUTH", ClientId: CLIENT_ID, AuthParameters: { USERNAME: MY_EMAIL, // username として設定した値ではなく、E メールアドレスを使用 PASSWORD: MY_PASSWORD, }, });
const response = await client.send(command); console.log(response);}
実行結果:ユーザーのメールアドレスにワンタイムパスワード(OTP)が送信されます。
MFA が有効な場合の実行結果:
レスポンスに Session
が含まれます。これは次のステップで RespondToAuthChallengeCommand
を実行する際に必要です。
$ pnpm tsx src/login/initiate-auth-command.ts{ '$metadata': { httpStatusCode: 200, requestId: '94eab882-6346-43d7-a177-be8b10d7c1bc', // ...省略 }, ChallengeName: 'EMAIL_OTP', ChallengeParameters: { CODE_DELIVERY_DELIVERY_MEDIUM: 'EMAIL', CODE_DELIVERY_DESTINATION: '****@****', USER_ID_FOR_SRP: '071e1507-4eb1-49b1-87bc-1b4740b4ffbf' }, Session: 'AYA...52w'}
2. 認証を完了する(RespondToAuthChallengeCommand)
Section titled “2. 認証を完了する(RespondToAuthChallengeCommand)”機能:メールで受け取った OTP を使用して認証を完了
重要なポイント:
USERNAME
にはメールアドレスを使用- 前のステップで取得した
Session
が必要
// ...省略
async function respondToAuthChallenge() {
const command = new RespondToAuthChallengeCommand({ ClientId: CLIENT_ID, ChallengeName: "EMAIL_OTP", Session: session, ChallengeResponses: { USERNAME: MY_EMAIL, // username として設定した値ではなく、E メールアドレスを使用 EMAIL_OTP_CODE: confirmationCode, // コマンドライン引数から取得した確認コードを使用 } });
const response = await client.send(command); console.log(response);}
実行結果:
スクリプトの第一引数に OTP コードを、第二引数に Session
を指定します。
これで認証が完了し、アクセストークン、ID トークン、リフレッシュトークンを取得できます。
$ session='AYA...52w'$ pnpm tsx src/login/respond-to-auth-challenge-command.ts [otp code] $session{ '$metadata': { httpStatusCode: 200, requestId: '15faf1b2-a2c6-48f4-a62c-544ae40746e8', // ...省略 }, AuthenticationResult: { AccessToken: 'eyJ...piw', ExpiresIn: 3600, IdToken: 'eyJ...nyQ', NewDeviceMetadata: { DeviceGroupKey: 'ce951d9b-dec0-4e2a-943f-3a43a05e4821', DeviceKey: 'ap-northeast-1_365fa23c-523e-46f0-a4e3-200308bffeff' }, RefreshToken: 'eyJ...w6g', TokenType: 'Bearer' }, ChallengeParameters: {}}
管理者コマンドの活用と注意点
Section titled “管理者コマンドの活用と注意点”管理者用コマンド:
使用方法:
$ pnpm tsx src/login/admin-initiate-auth-command.ts$ pnpm tsx src/login/admin-respond-to-auth-challenge-command.ts 631055 $session
注意点:
アプリケーションクライアントの認証フローでサーバー側の管理者認証情報(ALLOW_ADMIN_USER_PASSWORD_AUTH
)を有効にする必要があります。無効の場合、InvalidParameterException
が発生します。
$ pnpm tsx src/login/admin-respond-to-auth-challenge-command.ts 251150 $sessionInvalidParameterException: Missing required parameter EMAIL_OTP_CODE at de_InvalidParameterExceptionRes (/path/to/node_modules/@aws-sdk/client-cognito-identity-provider/dist-cjs/index.js:4720:21) at de_CommandError (/path/to/node_modules/@aws-sdk/client-cognito-identity-provider/dist-cjs/index.js:4416:19) at process.processTicksAndRejections (node:internal/process/task_queues:105:5) at async /path/to/node_modules/@smithy/middleware-serde/dist-cjs/index.js:36:20 at async /path/to/node_modules/@smithy/core/dist-cjs/index.js:193:18 at async /path/to/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38 at async /path/to/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:33:22 at async adminRespondToAuthChallenge (/path/to/src/login/admin-respond-to-auth-challenge-command.ts:26:20) { '$fault': 'client', '$metadata': { httpStatusCode: 400, requestId: '9a385696-1f7e-456b-8561-4563de2186b2', // ...省略 }, reasonCode: undefined, __type: 'InvalidParameterException'}
この記事では、Amazon Cognito User Pools を使用した TypeScript 認証システムの実装についてまとめました
実装したポイント:
- Terraform による Cognito User Pools の構築
- エイリアス属性を使用したメールアドレスログイン
- MFA(Email OTP)の有効化
- ユーザー登録フローの実装
- 認証フローの実装
重要な学び:
- ユーザープール作成後はログイン属性を変更できない
- エイリアス属性の使用により運用の柔軟性が向上
- MFA 設定はパスワードリセット方法に影響する
- 管理者コマンドを使用する場合は適切な認証フロー設定が必要