こんにちは、広野です。
生成 AI 界隈の技術の進化がすさまじく、以前開発したチャットボットのアーキテクチャも陳腐化が見えてきました。この記事を執筆している時点での最新のアーキテクチャで改めて作り直してみたので、いくつかの記事に分けて紹介します。
今回 (2回目) は実装編 その1 Amazon Cognito 編です。
大変恐縮ですが、AWS CloudFormation によるデプロイをしているので YAML テンプレートベースでの説明となります。ご了承ください。
前回の記事
アーキテクチャ概要については前回記事で紹介しています。こちらをご覧ください。
今回の説明範囲
アーキテクチャ図中、赤枠の部分を説明いたします。
セキュリティの根幹となる認証部分の説明になります。別件で公開している Amazon Cognito の説明記事とほぼ変わりませんが、あらためて掲載します。また、他の用途にも同じ構成で転用しやすいので一度覚えると有用です。ほんと万能です。
実装している仕様
- 図には Amazon Cognito ユーザープールしか表現していませんが、Amazon Cognito ID プールも作成しています。私が作成するアプリでは ID プールを使用した Amazon Cognito ユーザーまたはグループ単位の AWS リソースアクセス制御をすることが高いからです。ただし、本記事の構成では設定はほぼ何もなく、基本的な設定を入れているだけです。(細かい設定を入れる前の枠だけ作った感じ)
- Amazon Cognito でユーザーがセルフサインアップできるようにしています。その方がユーザー管理しなくて済み、楽だからです。代わりに、誰でも登録できるのは良くないので、メールアドレスのドメイン名 (本記事では scsk.jp) でセルフサインアップ可否を判断する AWS Lambda 関数を入れています。当然、セルフサインアップの過程でメールの存在確認をするようになっています。
- Amazon Cognito は管理者用のメール送信をする都合で、Amazon SES を使用しています。そのためのパラメータを入力する必要があります。Amazon SES は以下の参考記事のようにログ取得用の設定セットが作成されていることが前提となっています。
- ユーザーは MFA を強制されます。
- Amazon Cognito ユーザーグループは ADMIN と BASIC の 2種類を初期作成しており、後から ADMIN 所属でないとできない機能を作成するときに使用します。バックエンドやアプリ側で、Amazon Cognito ユーザーの属性を取得することで権限制御できます。
AWS CloudFormation テンプレート
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates a Cognito user pool, a Cognito ID pool, Lambda functions and relevant IAM roles. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SystemName: Type: String Description: System name. use lower case only. (e.g. example) Default: example MaxLength: 10 MinLength: 1 SubName: Type: String Description: System sub name. use lower case only. (e.g. prod or dev) Default: dev MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. xxxxx.xxx (e.g. example.com) Default: example.com MaxLength: 40 MinLength: 5 SesId: Type: String Description: Amazon SES ID for sending emails. (email addreess or domain) Default: example.com MaxLength: 100 MinLength: 5 SesConfigurationSet: Type: String Description: Amazon SES configuration set for sending emails. Default: ses-logs-xxxxxxxxxxxx-ap-northeast-1 MaxLength: 100 MinLength: 5 CognitoAdminAlias: Type: String Description: The alias name of Cognito Admin email address. (e.g. admin) Default: admin MaxLength: 100 MinLength: 5 CognitoReplyTo: Type: String Description: Cognito Reply-to email address. (e.g. xxx@xxx.xx) Default: xxxxxx@example.com MaxLength: 100 MinLength: 5 CognitoEmailFrom: Type: String Description: Cognito e-mail from address. (e.g. xxx@xxx.xx) Default: no-reply@mail.example.com MaxLength: 100 MinLength: 5 AllowedUserEmailDomains: Description: Domain list to allow user sign up. Each domains must be comma delimited and double quoted. Type: String Default: '"scsk.jp"' Resources: # ------------------------------------------------------------# # Cognito Idp Roles (IAM) # ------------------------------------------------------------# CognitoIdpAuthRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-CognitoIdpAuthRole-${SubName} Description: This role allows Cognito authenticated users to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": authenticated Policies: - PolicyName: !Sub ${SystemName}-CognitoIdpAuthRolePolicy-${SubName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" - "cognito-identity:*" Resource: "*" CognitoIdpUnauthRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-CognitoIdpUnauthRole-${SubName} Description: This role allows Cognito unauthenticated users to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": unauthenticated Policies: - PolicyName: !Sub ${SystemName}-CognitoIdpUnauthRolePolicy-${SubName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" - "cognito-identity:*" Resource: "*" CognitoGroupAdminRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-CognitoGroupAdminRole-${SubName} Description: This role allows Cognito authenticated users that belong to ADMIN group to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": authenticated Policies: - PolicyName: !Sub example-CognitoGroupBasicPolicy-${SubName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" - "cognito-identity:*" Resource: "*" CognitoGroupBasicRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-CognitoGroupBasicRole-${SubName} Description: This role allows Cognito authenticated users that belong to BASIC group to access AWS resources. AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Federated: cognito-identity.amazonaws.com Action: "sts:AssumeRoleWithWebIdentity" Condition: StringEquals: "cognito-identity.amazonaws.com:aud": !Ref IdPool "ForAnyValue:StringLike": "cognito-identity.amazonaws.com:amr": authenticated Policies: - PolicyName: !Sub ${SystemName}-CognitoIdpUnauthRolePolicy-${SubName} PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "mobileanalytics:PutEvents" - "cognito-sync:*" - "cognito-identity:*" Resource: "*" # ------------------------------------------------------------# # Lambda (triggered from Cognito) Role (IAM) # ------------------------------------------------------------# LambdaTriggeredFromCognitoRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${SystemName}-LambdaTriggeredFromCognitoRole-${SubName} Description: This role grants Lambda functions triggered from Cognito basic priviledges. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess - arn:aws:iam::aws:policy/AmazonCognitoPowerUser # ------------------------------------------------------------# # Cognito Lambda Invocation Permission # ------------------------------------------------------------# CognitoLambdaInvocationPermissionPresignup: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaCognitoPresignup.Arn Action: lambda:InvokeFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Ref AWS::AccountId SourceArn: !GetAtt UserPool.Arn DependsOn: - LambdaCognitoPresignup - UserPool CognitoLambdaInvocationPermissionPostconfirm: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaCognitoPostconfirm.Arn Action: lambda:InvokeFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Ref AWS::AccountId SourceArn: !GetAtt UserPool.Arn DependsOn: - LambdaCognitoPostconfirm - UserPool # ------------------------------------------------------------# # Cognito user pool # ------------------------------------------------------------# UserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: !Sub ${SystemName}-${SubName} MfaConfiguration: "ON" EnabledMfas: - SOFTWARE_TOKEN_MFA Policies: PasswordPolicy: MinimumLength: 8 RequireUppercase: true RequireLowercase: true RequireNumbers: true RequireSymbols: false TemporaryPasswordValidityDays: 180 AccountRecoverySetting: RecoveryMechanisms: - Name: verified_email Priority: 1 AdminCreateUserConfig: AllowAdminCreateUserOnly: false AutoVerifiedAttributes: - email DeviceConfiguration: ChallengeRequiredOnNewDevice: false DeviceOnlyRememberedOnUserPrompt: false EmailConfiguration: ConfigurationSet: !Ref SesConfigurationSet EmailSendingAccount: DEVELOPER From: !Sub "${SystemName}-${SubName} ${CognitoAdminAlias} <${CognitoEmailFrom}>" ReplyToEmailAddress: !Ref CognitoReplyTo SourceArn: !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/${SesId} EmailVerificationMessage: !Sub "${SystemName}-${SubName} Verification code: {####}" EmailVerificationSubject: !Sub "${SystemName}-${SubName} Verification code" LambdaConfig: PreSignUp: !GetAtt LambdaCognitoPresignup.Arn PostConfirmation: !GetAtt LambdaCognitoPostconfirm.Arn UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolAddOns: AdvancedSecurityMode: "OFF" UserPoolTags: Cost: !Sub ${SystemName}-${SubName} UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: UserPoolId: !Ref UserPool ClientName: !Sub ${SystemName}-${SubName}-appclient GenerateSecret: false RefreshTokenValidity: 3 AccessTokenValidity: 6 IdTokenValidity: 6 ExplicitAuthFlows: - ALLOW_USER_SRP_AUTH - ALLOW_REFRESH_TOKEN_AUTH PreventUserExistenceErrors: ENABLED SupportedIdentityProviders: - COGNITO CallbackURLs: - !Sub https://${DomainName}/index.html LogoutURLs: - !Sub https://${DomainName}/index.html DefaultRedirectURI: !Sub https://${DomainName}/index.html AllowedOAuthFlows: - implicit AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - email - openid # ------------------------------------------------------------# # Cognito user group # ------------------------------------------------------------# UserPoolGroupAdmin: Type: AWS::Cognito::UserPoolGroup Properties: Description: ${SystemName} User Group which allows users able to access management tools. GroupName: ADMIN Precedence: 1 UserPoolId: !Ref UserPool RoleArn: !GetAtt CognitoGroupAdminRole.Arn UserPoolGroupBasic: Type: AWS::Cognito::UserPoolGroup Properties: Description: ${SystemName} User Group which allows users able to access foundation and associate contents. GroupName: BASIC Precedence: 101 UserPoolId: !Ref UserPool RoleArn: !GetAtt CognitoGroupBasicRole.Arn # ------------------------------------------------------------# # Cognito id pool # ------------------------------------------------------------# IdPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub ${SystemName}-${SubName} AllowClassicFlow: false AllowUnauthenticatedIdentities: false CognitoIdentityProviders: - ClientId: !Ref UserPoolClient ProviderName: !GetAtt UserPool.ProviderName ServerSideTokenCheck: true IdentityPoolTags: - Key: Cost Value: !Sub ${SystemName}-${SubName} IdPoolRoleAttachment: Type: AWS::Cognito::IdentityPoolRoleAttachment Properties: IdentityPoolId: !Ref IdPool Roles: authenticated: !GetAtt CognitoIdpAuthRole.Arn unauthenticated: !GetAtt CognitoIdpUnauthRole.Arn RoleMappings: userpool: IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}:${UserPoolClient} Type: Token AmbiguousRoleResolution: AuthenticatedRole # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaCognitoPresignup: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${SystemName}-CognitoPresignup-${SubName} Description: !Sub Lambda Function triggered from Cognito before user self signup to check the user's email domain for ${SystemName}-${SubName} Runtime: python3.13 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaTriggeredFromCognitoRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} Code: ZipFile: !Sub | import re def lambda_handler(event, context): try: print(event) triggersource = event['triggerSource'] email = event['request']['userAttributes']['email'] print(email) if triggersource == 'PreSignUp_SignUp': print('via self signup') domain = email.split('@')[1] allowedDomains = [ ${AllowedUserEmailDomains} ] if domain in allowedDomains: if re.fullmatch(r'w[0-9]{5}@scsk\.jp',email): print('prohibited email account') return None else: print('allowed domain and email account') return event else: print('prohibited domain') return None else: print('via admin console') return event except Exception as e: print(str(e)) DependsOn: LambdaTriggeredFromCognitoRole LambdaCognitoPostconfirm: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${SystemName}-CognitoPostconfirm-${SubName} Description: !Sub Lambda Function triggered from Cognito after user confirmation to add the user in BASIC group for ${SystemName}-${SubName} Runtime: python3.13 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaTriggeredFromCognitoRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub ${SystemName}-${SubName} Code: ZipFile: | import boto3 client = boto3.client('cognito-idp') def lambda_handler(event, context): USERPOOLID = event['userPoolId'] USERNAME = event['userName'] try: print(event) res = client.admin_add_user_to_group( UserPoolId=USERPOOLID, Username=USERNAME, GroupName='BASIC' ) return event except Exception as e: print(str(e)) DependsOn: LambdaTriggeredFromCognitoRole # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # Cognito CognitoUserPoolID: Value: !Ref UserPool Export: Name: !Sub CognitoUserPoolId-${SystemName}-${SubName} CognitoArn: Value: !GetAtt UserPool.Arn Export: Name: !Sub CognitoArn-${SystemName}-${SubName} CognitoProviderName: Value: !GetAtt UserPool.ProviderName Export: Name: !Sub CognitoProviderName-${SystemName}-${SubName} CognitoProviderURL: Value: !GetAtt UserPool.ProviderURL Export: Name: !Sub CognitoProviderURL-${SystemName}-${SubName} CognitoAppClientID: Value: !Ref UserPoolClient Export: Name: !Sub CognitoAppClientId-${SystemName}-${SubName} CognitoIdPoolID: Value: !Ref IdPool Export: Name: !Sub CognitoIdPoolId-${SystemName}-${SubName}
続編記事
続編記事が出来次第、この章を更新します。
まとめ
いかがでしたでしょうか。
まだ本来の目的を達成するための前段階ですが、別件でも役に立つ構成だと思いますので改めて載せました。
本記事が皆様のお役に立てれば幸いです。