Skip to content

TypeScript で実装する Amazon Cognito User Pools 認証システム【MFA 対応・Terraform 構築】

業務で Cognito を使用した認証システムの実装を担当することになり、AWS Cognito をはじめて使用したため、ドキュメントを参考にしながら学んだ内容をまとめました。

https://github.com/kntks/blog-code/tree/main/2025/07/amazon-cognito-typescript-authentication-tutorial

項目バージョン
MacVentura 15.5
Terraform1.12.1
Node.js24.1.0
Terminal window
pnpm init
pnpm add @aws-sdk/client-cognito-identity-provider
pnpm add -D tsx typescript @types/node
# tsconfig.json作成
pnpm tsc --init

作成手順

  1. リソース定義ファイルを作成

  2. インフラを構築

    Terminal window
    cd terraform
    terraform init
    terraform plan
    terraform apply

主要な設定項目

terraform/cognito.tf
resource "aws_cognito_user_pool" "main" {
# ...省略
# NOTE: alias_attributesを設定している場合、username属性は設定できない
# username_attributes = []
alias_attributes = ["email"]
auto_verified_attributes = ["email", "phone_number"]
mfa_configuration = "OPTIONAL"
# ...省略
}

エイリアス属性

  • alias_attributes:ユーザーがログインに使用できる属性
  • 今回の設定:メールアドレス

自動確認属性

  • auto_verified_attributes:サインアップ時に自動確認する属性
  • 今回の設定:メールアドレスと電話番号

エイリアス属性を使用する理由

Section titled “エイリアス属性を使用する理由”

username 属性の問題点

  • ユーザー作成後に変更不可
  • メールアドレス変更に対応できない

エイリアス属性の利点

  • メールアドレスでログイン可能
  • サインアップ後にメールアドレス変更可能
  • 運用の柔軟性が向上

参考:ユーザー属性の操作 - AWS Documentation

1. 新規ユーザーを作成する(SignUpCommand)

Section titled “1. 新規ユーザーを作成する(SignUpCommand)”

機能:メールアドレスを使用してユーザーを新規作成

src/signup/signup-command.ts
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);
}

実行結果

Terminal window
$ 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'
}

処理結果

  • 確認ステータス:未確認
  • メールに確認コードが送信される

signup

email-otp

2. サインアップを確認する(ConfirmSignUpCommand)

Section titled “2. サインアップを確認する(ConfirmSignUpCommand)”

機能:メールで受信した確認コードを使用してサインアップを確認

src/signup/confirm-signup-command.ts
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);
}

実行結果

Terminal window
$ pnpm tsx src/signup/confirm-signup-command.ts 155675
{
'$metadata': {
httpStatusCode: 200,
requestId: '1d32ca34-e04f-4cbd-803f-5ff5288ecdeb',
// ...省略
}
}

確認後の状態

  • E メール確認済み:はい
  • 確認ステータス:確認済み

signup

ユーザープールの MFA 設定オプション

MFA を強制していないため、ユーザーごとに MFA を有効化できます。

mfa-option

MFA 有効化の実装

src/signup/set-user-mfa-preference-command.ts
// ...省略
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);
}

実行結果

Terminal window
$ pnpm tsx src/signup/set-user-mfa-preference-command.ts
{
'$metadata': {
httpStatusCode: 200,
requestId: '7d17f07b-ebe6-4727-b1e7-683ebac3e253',
// ...省略
}
}

目標

  • アプリケーションで MFA を強制する
  • サインアップ後にホーム画面へスムーズに遷移
  • セッションにアクセストークンを保持

課題

ConfirmSignUpCommand 実行後、アクセストークン取得にはログイン処理が必要です。これによりユーザーはサインアップ後に再度ログインが必要になり、ユーザビリティが低下します。

解決策

サーバー側で以下の処理を順次実行し、ユーザーの手間を省きます:

  1. ConfirmSignUpCommand を実行
  2. InitiateAuthCommandAdminInitiateAuthCommand を実行(アクセストークン取得)
    • ユーザープールで MFA を強制した場合、MFA コード入力が必要
  3. SetUserMFAPreferenceCommand を実行(MFA 有効化)

効果サインアップ後に直接ホーム画面に遷移可能

管理者機能と重複メールアドレスの処理

Section titled “管理者機能と重複メールアドレスの処理”

ConfirmSignUpCommand では、alias 属性に設定したメールアドレスが既存の場合、AliasExistsException が発生します。

解決方法

  • AliasExistsException でエラーハンドリング
  • ForceAliasCreation オプションを使用して alias 作成を強制

AdminConfirmSignUpCommand では、管理者権限でユーザー確認を強制できます。

テスト条件:同じメールアドレスを持つ別のユーザーを作成

src/signup/signup-command.ts
// ...省略
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,
},
],
});
// ...省略
}

signup-same-email-user

1. ForceAliasCreation なしの場合

src/signup/confirm-signup-command.ts
// ...省略
async function confirmSignUpUser() {
const command = new ConfirmSignUpCommand({
ClientId: CLIENT_ID,
Username: MY_USERNAME_A,
Username: MY_USERNAME_B,
ConfirmationCode: confirmationCode,
});
// ...省略
}

結果AliasExistsException が発生

Terminal window
$ pnpm tsx src/signup/confirm-signup-command.ts 827653
AliasExistsException: 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 を実行します。

src/signup/confirm-signup-command.ts
// ...省略
async function confirmSignUpUser() {
const command = new ConfirmSignUpCommand({
ClientId: CLIENT_ID,
Username: MY_USERNAME_A,
Username: MY_USERNAME_B,
ConfirmationCode: confirmationCode,
ForceAliasCreation: true,
});
// ...省略
}

結果

  • E メール確認済み:はい
  • 確認ステータス:確認済み

signup-same-email-user-2

3. AdminConfirmSignUpCommand の場合

ユーザーを再作成後、AdminConfirmSignUpCommand を実行します。

src/signup/admin-confirm-signup-command.ts
async function adminConfirmSignUp() {
const command = new AdminConfirmSignUpCommand({
UserPoolId: USER_POOL_ID,
Username: MY_USERNAME_B
});
const response = await client.send(command);
console.log(response);
}
Terminal window
$ pnpm tsx src/signup/admin-confirm-signup-command.ts
{
'$metadata': {
httpStatusCode: 200,
requestId: '2f660d4d-da03-4388-87ad-a7638b7e1ba7',
// ...省略
}
}

結果

  • E メール確認済み:いいえ
  • 確認ステータス:確認済み

signup-same-email-user-3

1. ログインを開始する(InitiateAuthCommand)

Section titled “1. ログインを開始する(InitiateAuthCommand)”

機能:ユーザー名とパスワードを使用してログインを開始

src/login/initiate-auth-command.ts
// ...省略
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 を実行する際に必要です。

Terminal window
$ 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 が必要
src/login/respond-to-auth-challenge-command.ts
// ...省略
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 トークン、リフレッシュトークンを取得できます。

Terminal window
$ 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 “管理者コマンドの活用と注意点”

管理者用コマンド

使用方法

Terminal window
$ 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 が発生します。

Terminal window
$ pnpm tsx src/login/admin-respond-to-auth-challenge-command.ts 251150 $session
InvalidParameterException: 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 設定はパスワードリセット方法に影響する
  • 管理者コマンドを使用する場合は適切な認証フロー設定が必要