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 --initCognito 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 設定はパスワードリセット方法に影響する
 - 管理者コマンドを使用する場合は適切な認証フロー設定が必要