AWS Cognitoを用いたユーザー認証アダプター実装例

## 概要
AWS SDK for JavaScript (v3) と Cognito を用いてユーザー認証・管理を行うクラス CognitoAdapter の実装例を紹介する。

実装例は以下の機能をサポートしている。

- ユーザー登録 (signup)
- 登録完了承認コードの確認 (confirmSignup)
- 登録完了承認コードの再送 (resendConfirmationCode)
- ログイン (login)
- RefreshTokenを用いたAccessTokenの再取得 (refreshAccessToken)
- AccessTokenの検証 (verifyAccessToken)


## 前提条件
- Cognito User Pool, App Client (Client Secret有り) が設定済み
- 環境変数として以下が設定済み
  - `AWS_REGION`: CognitoがデプロイされているAWSリージョン
  - `AWS_COGNITO_USER_POOL_ID`: 対象User PoolのID
  - `AWS_COGNITO_CLIENT_ID`: 対象App ClientのID
  - `AWS_COGNITO_CLIENT_SECRET`: 対象App Clientに紐づくClient Secret


## 使用ライブラリ
- `@aws-sdk/client-cognito-identity-provider`: CognitoのAPIクライアント
- `aws-jwt-verify`: JWTトークンの検証を行うライブラリ
- `crypto`: 暗号化ライブラリ


## コード例
```
import {
  CognitoIdentityProviderClient,
  SignUpCommand,
  ConfirmSignUpCommand,
  ResendConfirmationCodeCommand,
  InitiateAuthCommand,
} from '@aws-sdk/client-cognito-identity-provider'
import crypto from 'crypto'
import { getEnv } from '@/backend/utils/env'  // 自前の環境変数取得用のヘルパー
import { CognitoJwtVerifier } from 'aws-jwt-verify'

export class CognitoAdapter {
  private client: CognitoIdentityProviderClient
  private clientId: string
  private clientSecret: string
  private verifier: CognitoJwtVerifier

  constructor() {
    this.client = new CognitoIdentityProviderClient({
      region: getEnv('AWS_REGION'),
    })
    this.clientId = getEnv('AWS_COGNITO_CLIENT_ID')
    this.clientSecret = getEnv('AWS_COGNITO_CLIENT_SECRET')
    this.verifier = CognitoJwtVerifier.create({
      userPoolId: getEnv('AWS_COGNITO_USER_POOL_ID'),
      tokenUse: 'access',
      clientId: this.clientId,
    })
  }

  // ユーザー登録
  async signup(email: string, password: string) {
    const secretHash = this.#calculateSecretHash(email)
    const command = new SignUpCommand({
      SecretHash: secretHash,
      ClientId: this.clientId,
      Username: email,
      Password: password,
    })
    return await this.client.send(command)
  }

  // ユーザー登録承認
  async confirmSignup(email: string, confirmationCode: string) {
    const secretHash = this.#calculateSecretHash(email)
    const command = new ConfirmSignUpCommand({
      SecretHash: secretHash,
      ClientId: this.clientId,
      Username: email,
      ConfirmationCode: confirmationCode,
    })
    return await this.client.send(command)
  }

  // ユーザー登録用の認証コード再送
  async resendConfirmationCode(email: string) {
    const secretHash = this.#calculateSecretHash(email)
    const command = new ResendConfirmationCodeCommand({
      SecretHash: secretHash,
      ClientId: this.clientId,
      Username: email,
    })
    return await this.client.send(command)
  }

  // ログイン（ユーザーパスワード認証）
  async login(email: string, password: string) {
    const secretHash = this.#calculateSecretHash(email)
    const command = new InitiateAuthCommand({
      ClientId: this.clientId,
      AuthFlow: 'USER_PASSWORD_AUTH',
      AuthParameters: {
        SECRET_HASH: secretHash,
        USERNAME: email,
        PASSWORD: password,
      },
    })
    return await this.client.send(command)
  }

  // RefreshTokenを用いてAccessToken再取得
  async refreshAccessToken(refreshToken: string, sub: string) {
    const secretHash = this.#calculateSecretHash(sub)
    const command = new InitiateAuthCommand({
      ClientId: this.clientId,
      AuthFlow: 'REFRESH_TOKEN_AUTH',
      AuthParameters: {
        SECRET_HASH: secretHash,
        REFRESH_TOKEN: refreshToken,
      },
    })
    return await this.client.send(command)
  }

  // AccessTokenを検証
  async verifyAccessToken(accessToken: string) {
    return await this.verifier.verify(accessToken)
  }

  // SecretHash 計算
  #calculateSecretHash(email: string) {
    const message = email + this.clientId
    const hmac = crypto.createHmac('sha256', this.clientSecret)
    hmac.update(message)
    return hmac.digest('base64')
  }
}

```

## 主な処理のポイント
### 1. Clientインスタンスの生成
CognitoIdentityProviderClient を使用して、Cognitoと通信するためのクライアントを生成しています。

### 2. Secret Hashの生成
CognitoのApp Clientに「シークレットキー」が有効になっている場合、SECRET_HASH を計算する必要があります。
#calculateSecretHash() メソッドは、email + clientId を鍵としてHMAC-SHA256で署名し、その結果をBase64エンコードしたものをSecretHashとして返します。

### 3. ユーザー登録・承認フロー
signup() でユーザー登録
confirmSignup() でメール等で届いた確認コードを用いて登録を承認
resendConfirmationCode() で承認コードの再送
これらはCognitoによる標準的なユーザーオンボーディングフローを実現します。

### 4. ログインおよびRefresh TokenでのAccessToken再取得
login() は、USER_PASSWORD_AUTH フローでログインします。
成功時には AccessToken, IdToken, RefreshToken などが返却されます。
refreshAccessToken() により、失効前の RefreshToken を使って新たな AccessToken を取得可能です。

### 5. AccessToken検証
verifyAccessToken() は、aws-jwt-verify を用いてトークンがCognitoで発行されたものであることを検証します。
CognitoJwtVerifier.create() で tokenUse や userPoolId、clientId を指定しているため、不正なトークンを弾くことができます。


## まとめ
本記事では、AWS Cognitoを用いたユーザー認証処理を抽象化するクラスCognitoAdapterの実装例を示しました。
このクラスを利用することで、サーバーサイドのユーザー登録、認証、トークン検証といった処理を簡潔に扱うことができます。

参考になれば幸いです。