Cognitoの構築方法を忘れないように、Terraformを使ってコード化しておこうと思います。 また、せっかくなのでAPIGatewayを前段に配置してCognitoをオーソライザとして使ってみるところまでを試してみようと思います。 こちらの記事(Part1)ではCognitoの構築をメインに記述します。(※今回IDプールは使用しませんが後学のためにコード化だけしています。) Part2ではAPIGatewayの構築とCognitoのオーソライザ設定を主に記述します。
フォルダ構成
terraform/ ├ modules/ │ └ cognito/ │ ├ iam.tf │ ├ locals.tf │ ├ main.tf │ ├ output.tf │ └ variable.tf ├ main.tf ├ variables.tf ├ output.tf └ provider.tf
APIGatewayやLambdaも次のPartで構築する予定のためモジュール化しています。
コード説明
メイン
provider.tf
terraformのバージョンやawsのリージョン、プロファイルを指定しています。
terraform { required_version = ">= v1.2.3" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.14" } } } # Configure the AWS Provider provider "aws" { # リージョン region = var.region profile = var.profile }
main.tf
cognitoモジュールに、variable.tfで設定された値をセットしています。
module "cognito" { source = "./modules/cognito" region = var.region # 不要であれば空配列([])を指定する string_schemas = var.string_schemas number_schemas = var.number_schemas another_schemas = var.another_schemas # 使用しない場合は空配列([])を指定する allowed_oauth_flows = var.allowed_oauth_flows read_attributes = var.read_attributes write_attributes = var.write_attributes }
variable.tf
main.tfをすっきりさせたかったので、コード量が多くなりそうなフィールドをピックアップしてこちらに記述しています。
# ----------------------------------------------- # プロバイダー設定 # ----------------------------------------------- variable "region" { type = string default = "ap-northeast-1" description = "リージョン" } variable "profile" { type = string default = "default" description = "プロファイル" } # ----------------------------------------------- # cognito ユーザプール設定値 # ----------------------------------------------- variable "string_schemas" { type = list(object({ name = string attribute_data_type = string #Boolean, Number, String, DateTime. developer_only_attribute = bool mutable = bool required = bool string_attribute_constraints = object ({ max_length = number min_length = number }) })) default = [ { attribute_data_type = "String" name = "sample" developer_only_attribute = false mutable = true # false for "sub" required = false # true for "sub" string_attribute_constraints = { # if it is a string min_length = 0 # 10 for "birthdate" max_length = 2048 # 10 for "birthdate" } } ] description = "ユーザに登録を求める属性情報(string型)" } variable "number_schemas" { type = list(object({ name = string attribute_data_type = string #Boolean, Number, String, DateTime. developer_only_attribute = bool mutable = bool required = bool number_attribute_constraints = object({ max_length = number min_length = number }) })) default = [ ] description = "ユーザに登録を求める属性情報(number型)" } variable "another_schemas" { type = list(object({ name = string attribute_data_type = string #Boolean, Number, String, DateTime. developer_only_attribute = bool mutable = bool required = bool })) default = [ ] description = "ユーザに登録を求める属性情報(bool, datetime型)" } # ----------------------------------------------- # アプリケーションクライアントの設定 # ----------------------------------------------- variable "allowed_oauth_flows" { type = list default = ["code"] description = "OAuthフローの許可(※空の場合はfalse)" } variable "read_attributes" { type = map(bool) default = { "address" = true "birthdate" = true "email" = true "email_verified" = true "family_name" = true "gender" = true "given_name" = true "locale" = true "middle_name" = true "name" = true "nickname" = true "phone_number" = true "phone_number_verified" = true "picture" = true "preferred_username" = true "profile" = true "updated_at" = true "website" = true "zoneinfo" = true } description = "ユーザ属性の読み取り可否※カスタム属性で追加したものを読み取り可能にしたい場合はここにも追加すること" } variable "write_attributes" { type = map(bool) default = { "address" = true "birthdate" = true "email" = true # "email_verified" = true ※書き込み不可項目 "family_name" = true "gender" = true "given_name" = true "locale" = true "middle_name" = true "name" = true "nickname" = true "phone_number" = true # "phone_number_verified" = true ※書き込み不可項目 "picture" = true "preferred_username" = true "profile" = true "updated_at" = true "website" = true "zoneinfo" = true } description = "ユーザ属性の書き込み可否(※カスタム属性で追加したものを書き込み可能にしたい場合はここにも追加すること)" }
output.tf
環境構築後、取得したい情報をまとめています。
output user_pool_id { value = module.cognito.user_pool_id } output identity_pool_id { value = module.cognito.identity_pool_id } output client_id { value = module.cognito.client_id } output domain { value = module.cognito.domain }
出力内容は
ユーザプールID
IDプールID
クライアントID
ユーザプールドメイン(今回Hosted UIを使用するので出力している)
モジュール
modules/cognito/main.tf
ユーザプール、IDプール、クライアント、ドメイン、Hosted UIを定義します。(※Hosted UI作成にはドメインが必要) ドメインには適宜有効な値を設定する必要があります。
# ユーザプール定義 resource "aws_cognito_user_pool" "sample_user_pool" { name = "sample-pool" # ユーザ確認のための自動検証をメールで行う auto_verified_attributes = ["email"] # MFAの有効・無効 # (software_token_mfa_configurationまたはsms_configurationの少なくとも一つは設定しておく必要がある) mfa_configuration = "OFF" # 検証時のメッセージテンプレートを設定する verification_message_template { default_email_option = "CONFIRM_WITH_CODE" # CONFIRM_WITH_CODE or CONFIRM_WITH_LINK email_message = "{####}" # [Optional] プレースホルダー「{####}」を必ず含める email_message_by_link = "{##Click Here##}" # [Optional] プレースホルダー「{##Click Here##}」を必ず含める email_subject = "verify code" # [Optional] 検証コードを使用する場合のEメールの件名 email_subject_by_link = "verify link" # [Optional] 検証リンクをを使用する場合の件名 sms_message = "{####}" # [Optional] SMSを使用する場合のメッセージ プレースホルダー「{####}」を必ず含める } admin_create_user_config { # ユーザーに自己サインアップを許可する allow_admin_create_user_only = true # 招待メッセージのテンプレート invite_message_template { email_message = "Username is {username}. Temporary password is {####}." email_subject = "Temporary password" sms_message = "Username is {username}. Temporary password is {####}." } } # メール送信元の設定(AWS SESを使用する場合は「DEVELOPER」) email_configuration { email_sending_account = "COGNITO_DEFAULT" } # パスワードポリシー password_policy { minimum_length = 8 # 最低文字数 require_lowercase = true # 英小文字 require_numbers = true # 数字 require_symbols = true # 記号 require_uppercase = true # 英大文字 temporary_password_validity_days = 7 # 初期登録時の一時的なパスワードの有効期限 } # 「schema」は登録するユーザーに求める属性 # ここでは例として標準属性のemailを必須に設定している schema { attribute_data_type = "String" name = "email" required = true } # string型のカスタム属性は「string_attribute_constraints」を使用して設定する # ※以下のようにdynamicブロックを使用することで、リストが空の場合は定義しないように設定できる dynamic "schema" { for_each = var.string_schemas content { attribute_data_type = schema.value.attribute_data_type name = schema.value.name required = schema.value.required developer_only_attribute = schema.value.developer_only_attribute mutable = schema.value.mutable string_attribute_constraints { max_length = schema.value.string_attribute_constraints.max_length min_length = schema.value.string_attribute_constraints.min_length } } } # number型のカスタム属性は「number_attribute_constraints」を使用して設定する dynamic "schema" { for_each = var.number_schemas content { attribute_data_type = schema.value.attribute_data_type name = schema.value.name required = schema.value.required developer_only_attribute = schema.value.developer_only_attribute mutable = schema.value.mutable number_attribute_constraints { max_length = schema.value.number_attribute_constraints.max_length min_length = schema.value.number_attribute_constraints.min_length } } } # string, number以外の型のカスタム属性(bool, datetime)はconstraintsを設定する必要はない dynamic "schema" { for_each = var.another_schemas content { attribute_data_type = schema.value.attribute_data_type name = schema.value.name required = schema.value.required developer_only_attribute = schema.value.developer_only_attribute mutable = schema.value.mutable } } username_configuration { # ユーザ名(Email)の大文字小文字の区別をするかどうか case_sensitive = false } tags = { Env = "env" } } # ドメイン定義 resource "aws_cognito_user_pool_domain" "sample_user_pool_domain" { # 適宜有効な値を設定する domain = "xxxxxxxxxxxx" user_pool_id = aws_cognito_user_pool.sample_user_pool.id } # クライアント定義 resource "aws_cognito_user_pool_client" "sample_client" { name = "sample-client" user_pool_id = aws_cognito_user_pool.sample_user_pool.id # デフォルトは時間単位(※token_validity_units.access_tokenで単位の変更が可能) access_token_validity = 1 # デフォルトは時間単位(※token_validity_units.id_tokenで単位の変更が可能) id_token_validity = 1 # デフォルトは日単位(※token_validity_units.refresh_tokenで単位の変更が可能) refresh_token_validity = 30 # seconds, minutes, hours, daysから設定 token_validity_units { access_token = "hours" id_token = "hours" refresh_token = "days" } # フローが設定されていたらtrue、設定されていなければfalseとなるようにしておく allowed_oauth_flows_user_pool_client = length(var.allowed_oauth_flows) == 0 ? false : true allowed_oauth_flows = var.allowed_oauth_flows allowed_oauth_scopes = ["phone", "email", "openid", "profile"] # フローのタイムアウトを3分に設定 auth_session_validity = 3 callback_urls = ["http://localhost:3000"] # callback_urlsに含まれているものを指定すること default_redirect_uri = "http://localhost:3000" logout_urls = ["http://localhost:3000"] enable_token_revocation = true explicit_auth_flows = ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_PASSWORD_AUTH"] generate_secret = false # ユーザが存在しない場合の挙動(ENABLED or LEGACY) # ENABLED=ユーザ存在エラーを防止(ユーザ名、パスワードの間違いとして認証できない場合と同じ挙動となる) # LEGACY=UserNotFoundExceptionが返る prevent_user_existence_errors = "ENABLED" supported_identity_providers = ["COGNITO"] read_attributes = local.read_attribute_list write_attributes = local.write_attribute_list } # Hosted UI resource "aws_cognito_user_pool_ui_customization" "sample_hosted_ui" { client_id = aws_cognito_user_pool_client.sample_client.id css = ".label-customizable {font-weight: 400;}" # image_file = filebase64("logo.png") # Refer to the aws_cognito_user_pool_domain resource's # user_pool_id attribute to ensure it is in an 'Active' state user_pool_id = aws_cognito_user_pool_domain.sample_user_pool_domain.user_pool_id } # IDプール定義 resource "aws_cognito_identity_pool" "sample_identity_pool" { identity_pool_name = "sample-identity-pool" # 認証していないユーザーに使用を許可するか allow_unauthenticated_identities = false openid_connect_provider_arns = [] saml_provider_arns = [] supported_login_providers = {} tags = { Env = "env" } cognito_identity_providers { client_id = aws_cognito_user_pool_client.sample_client.id provider_name = "cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.sample_user_pool.id}" server_side_token_check = false } }
modules/cognito/variable.tf
メインコードから渡される値を格納する変数を定義します。
variable region {} # ユーザプール variable string_schemas {} variable number_schemas {} variable another_schemas {} # アプリケーションクライアント variable allowed_oauth_flows {} variable read_attributes {} variable write_attributes {}
modules/cognito/locals.tf
読み取り・書き込みの設定はリスト形式なのでmap→listに変換しています。
# 書き込み・読み取りぞれぞれでvalueがtrueのものをfilterして、属性(key)のリストを作成している locals { read_attribute_list = [for key, value in var.read_attributes : key if value == true] write_attribute_list = [for key, value in var.write_attributes : key if value == true] }
modules/cognito/output.tf
メインコードから参照できるように出力を定義しておきます。
# ※user_pool_arnはAPIGateway作成時に必要 output user_pool_arn { value = aws_cognito_user_pool.sample_user_pool.arn } output user_pool_id { value = aws_cognito_user_pool.sample_user_pool.id } output client_id { value = aws_cognito_user_pool_client.sample_client.id } output identity_pool_id { value = aws_cognito_identity_pool.sample_identity_pool.id } output domain { value = aws_cognito_user_pool_domain.sample_user_pool_domain.domain }
modules/cognito/iam.tf
Cognito IDプールが付与することができる権限を定義したロールを作成しています。(main.tfのコード量が多くて見にくくなるので分けました)
# 信頼関係 data "aws_iam_policy_document" "authenticated" { statement { effect = "Allow" principals { type = "Federated" identifiers = ["cognito-identity.amazonaws.com"] } actions = ["sts:AssumeRoleWithWebIdentity"] condition { test = "StringEquals" variable = "cognito-identity.amazonaws.com:aud" values = [aws_cognito_identity_pool.sample_identity_pool.id] } condition { test = "ForAnyValue:StringLike" variable = "cognito-identity.amazonaws.com:amr" values = ["authenticated"] } } } resource "aws_iam_role" "authenticated" { name = "cognito_authenticated" assume_role_policy = data.aws_iam_policy_document.authenticated.json } # 付与する権限 data "aws_iam_policy_document" "authenticated_role_policy" { statement { effect = "Allow" actions = [ "cognito-identity:GetOpenIdToken", "cognito-identity:GetCredentialsForIdentity" ] resources = ["*"] } } # 信頼関係と付与する権限を紐づけ resource "aws_iam_role_policy" "authenticated" { name = "authenticated_policy" role = aws_iam_role.authenticated.id policy = data.aws_iam_policy_document.authenticated_role_policy.json } # idプールにアタッチする resource "aws_cognito_identity_pool_roles_attachment" "id_pool_role_attachment" { identity_pool_id = aws_cognito_identity_pool.sample_identity_pool.id role_mapping { # aws_cognito_identity_pool.identity_pool.cognito_identity_providers.provider_nameに依存する identity_provider = "cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.sample_user_pool.id}:${aws_cognito_user_pool_client.sample_client.id}" ambiguous_role_resolution = "AuthenticatedRole" type = "Rules" mapping_rule { claim = "isAdmin" match_type = "Equals" role_arn = aws_iam_role.authenticated.arn value = "paid" } } roles = { "authenticated" = aws_iam_role.authenticated.arn } }
terraform コマンド実行
メインコード(今回の場合は,/terraform)のある階層で以下のコマンド実行します。(※planを実行して問題なければapplyを実行する)
terraform init terraform plan terraform apply . . . Apply complete! Resources: 8 added, 0 changed, 0 destroyed. Outputs: client_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" domain = "xxxxxxxxxxxx" identity_pool_id = "ap-northeast-1:xxxxxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxxx" user_pool_id = "ap-northeast-1_xxxxxxxxxx"
構築した環境を削除する場合はdestroy
terraform destroy
まとめ
Cognitoの設定は項目が多くて大変ですが、その分コード化しておくことで次回作業時の手間を大幅削減できるなと思いました。 またIDプールのiamの設定に関しては手動で環境構築していた際もハマりポイントだったので、ここで自動化のノウハウを得れてよかったです。 コード化すると理解がより進むような気がします。
参考
【AWS】TerraformでCognitoをつくって楽したいんだ! | Katsuya Place
Cognito ユーザープールをオーソライザーとして使って API Gateway のアクセスを制御する - AWSの部屋