こしあん
2024-12-19

CognitoのユーザーをTerraformで事前定義する


3{icon} {views}

TerraformでCognitoユーザーを登録し、初期パスワードを設定する方法を解説。 Pythonでパスワード変更とサインインを行う具体的な手順を紹介します。

はじめに

  • 一番簡単なCognitoの例を試すでCognitoを使ってユーザー登録から、アドレス認証、サインインまでをPythonベースで行った
  • 今回はもう少し具体的な例で、Terraformでユーザーのメールアドレスと初期パスワードを設定する。そのあとにパスワードの変更と、サインインまでを同じくPythonベースで行う
  • 前回との違いは以下の通り。今回は運用側で決められたユーザーに対してサービス提供するということを想定したケース
前回 今回
ユーザー登録 ユーザー側(signup.py) 運用側(Terraform)
メールアドレス認証 ユーザー側(confirn_user.py) 認証済みと自動設定(Terraform)
パスワード変更 ユーザー側(password_reset.py)
サインイン ユーザー側(signin.py) ユーザー側(signin.py)
  • 今回の流れは以下の通り
    • Terraformでユーザーと初期パスワードを事前定義しておく(メールアドレスとパスワードの組み合わせ)
    • terraform applyするとそのユーザーに対して初期パスワードのメール送信が走る(AWSで自動的に行なってくれる)
    • その初期パスワードを使って、ユーザーがパスワード変更を行う(password_reset.py)。初期パスワード、新しいパスワードを入力して変更する。このパスワード変更をしないと次のサインインには進めない
    • パスワードを変更後、サインインを行う(signin.py)。これによりアクセスキーなどの一時認証情報が払い出される。

ディレクトリ構成

├── main.tf             # メインのTerraform設定ファイル
├── cognito.tf          # AWS Cognito関連のTerraform設定ファイル
├── change_password.py  # パスワード変更処理を行うPythonスクリプト
└── signin.py           # サインイン処理を行うPythonスクリプト

Terraform部分

main.tfで以下のようにユーザー定義しておく。プロジェクトが大きくなったらterraform.tfvarsで行うのがいいだろう。初期パスワードハードコーディングもセキュリティ的にはよろしくなく、random_passwordなどを使うのが(参考)良いが、今回は説明用にハードコードした例で簡略化して説明する。

variable "users" {
  description = "Cognitoユーザープールに追加するユーザーのマップ。キーがメールアドレス、値が一時パスワード"
  type = map(string)
  default = {
    "user1@example.com" = "TemporaryPassword1!" # 実際にメールが届くアドレスを設定
    "user2@example.com" = "TemporaryPassword2!"
  }
}

ユーザープールを以下のように定義する(cognito.tf

# Cognitoユーザープールの作成
resource "aws_cognito_user_pool" "user_pool" {
  name = "my-first-user-pool"

  # パスワードポリシーの設定(必要に応じて調整)
  password_policy {
    minimum_length    = 8
    require_uppercase = true
    require_lowercase = true
    require_numbers   = true
    require_symbols   = true
    temporary_password_validity_days = 30
  }

  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }
}

# ユーザープールクライアントの作成
resource "aws_cognito_user_pool_client" "user_pool_client" {
  name         = "my-first-user-pool-client"
  user_pool_id = aws_cognito_user_pool.user_pool.id

  # 必要に応じて他のパラメータを設定
  explicit_auth_flows = [
    "ALLOW_USER_PASSWORD_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH"
  ]

  # リフレッシュトークンの有効期間(秒)
  refresh_token_validity = 30

  # クライアントシークレットの無効化(シンプルな例のため)
  generate_secret = false

  prevent_user_existence_errors = "ENABLED"
}

# users.tf

resource "aws_cognito_user" "users" {
  for_each = var.users

  user_pool_id = aws_cognito_user_pool.user_pool.id
  username     = each.key

  attributes = {
    email          = each.key
    email_verified = "true"  # メールを自動的に検証済みとして設定
  }

  temporary_password = each.value
}

# 出力変数の定義
output "user_pool_id" {
  description = "Cognito User Pool ID"
  value       = aws_cognito_user_pool.user_pool.id
}

output "user_pool_client_id" {
  description = "Cognito User Pool Client ID"
  value       = aws_cognito_user_pool_client.user_pool_client.id
}

新しい部分はaws_cognito_user.usersで、ここでユーザーをTerraform側で定義する。また、email_varifiedtrueにすると、自動的にメールアドレスが認証済みとして扱われる。

実際にterraform applyを行うと、以下のように対象のメールアドレスに対して初期パスワードが通知される。

同様にユーザープールのIDとユーザープールのクライアントIDが出てくるのでメモする。

user_pool_client_id = "1example23456789"
user_pool_id = "ap-northeast-1_ExaMPle"

Pythonコード部分

パスワード変更(change_password.py)

初期パスワードで後述のサインインを行おうとすると以下のようなエラーが出るはずだ。強制的にパスワードリセットの状態になっている。

{‘ChallengeName’: ‘NEW_PASSWORD_REQUIRED’, ‘Session’: …

# change_password.py
import boto3
from botocore.exceptions import ClientError

CLIENT_ID = 'YOUR_APP_CLIENT_ID'        # Terraformの出力から取得
PROFILE_NAME = 'hogehoge'

session = boto3.Session(profile_name=PROFILE_NAME)
client = session.client('cognito-idp')

def change_password(username, temporary_password, new_password):
    try:
        # 初回サインイン時に認証を開始
        response = client.initiate_auth(
            ClientId=CLIENT_ID,
            AuthFlow='USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username,
                'PASSWORD': temporary_password
            },
        )

        # 新しいパスワードが要求されるチャレンジに対応
        if response.get('ChallengeName') == 'NEW_PASSWORD_REQUIRED':
            response = client.respond_to_auth_challenge(
                ClientId=CLIENT_ID,
                ChallengeName='NEW_PASSWORD_REQUIRED',
                Session=response['Session'],
                ChallengeResponses={
                    'USERNAME': username,
                    'NEW_PASSWORD': new_password
                }
            )
            print("パスワード変更が成功しました。")
        else:
            print("予期しない認証フローの応答がありました。")
    except ClientError as e:
        print(f"パスワード変更エラー: {e.response['Error']['Message']}")

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Cognitoパスワード変更スクリプト")
    parser.add_argument('username', type=str, help='ユーザー名(メールアドレス)')
    parser.add_argument('temporary_password', type=str, help='一時パスワード')
    parser.add_argument('new_password', type=str, help='新しいパスワード')

    args = parser.parse_args()
    change_password(args.username, args.temporary_password, args.new_password)

パスワードの変更は以下のようにする。「パスワード変更が成功しました。」と表示されるはずだ。

python change_password.py user1@example.com TemporaryPassword1! NewSecurePassword1!

サインイン(signin.py)

あとは同様にサインインする。

# signin.py
import boto3
from botocore.exceptions import ClientError

# Terraformで出力された値を使用
CLIENT_ID = 'YOUR_APP_CLIENT_ID'        # 例: '1example23456789'
PROFILE_NAME = 'hogehoge'

session = boto3.Session(profile_name=PROFILE_NAME)
client = session.client('cognito-idp')

def sign_in(username, password):
    try:
        response = client.initiate_auth(
            ClientId=CLIENT_ID,
            AuthFlow='USER_PASSWORD_AUTH',
            AuthParameters={
                'USERNAME': username,
                'PASSWORD': password
            },
        )
        print(response)
        id_token = response['AuthenticationResult']['IdToken']
        access_token = response['AuthenticationResult']['AccessToken']
        refresh_token = response['AuthenticationResult']['RefreshToken']
        print("サインイン成功。")
        print(f"IDトークン: {id_token}")
        print(f"アクセストークン: {access_token}")
        print(f"リフレッシュトークン: {refresh_token}")
    except ClientError as e:
        error = e.response['Error']['Code']
        if error == 'NotAuthorizedException':
            print("パスワードが正しくありません。")
        elif error == 'UserNotFoundException':
            print("ユーザーが存在しません。")
        elif error == 'PasswordResetRequiredException':
            print("パスワードのリセットが必要です。")
        else:
            print(f"サインインエラー: {e.response['Error']['Message']}")

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Cognitoサインインスクリプト")
    parser.add_argument('username', type=str, help='ユーザー名(メールアドレス)')
    parser.add_argument('password', type=str, help='パスワード')

    args = parser.parse_args()
    sign_in(args.username, args.password)

サインインコマンドは以下の通り。

python signin.py user1@example.com NewSecurePassword1!

成功すると以下のように認証情報が払い出される。

{'ChallengeParameters': {}, 'AuthenticationResult': {'AccessToken': 'eyJraWQiOiJyK2c3QUUzY.....', 'IdToken': 'eyJraWQiOiJTTkV1dE......'}, 'ResponseMetadata': {'RequestId': 'd0ba2c6d-ae3e-4f41-b9cb-1064de71c87e', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Wed, 18 Dec 2024 04:32:17 GMT', 'content-type': 'application/x-amz-json-1.1', 'content-length': '4078', 'connection': 'keep-alive', 'x-amzn-requestid': 'd0ba2c6d-ae3e-4f41-b9cb-1064de71c87e'}, 'RetryAttempts': 0}}
サインイン成功。

おわりに

  • なんかCognitoのことをわかってきたので、次はALBとの統合あたりを試してみたい


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です