こんにちは、広野です。
先日、AWS Amplify + Amazon Cognito + React アプリのサインイン機能に あらかじめ登録したドメイン名のメールアドレスを持つユーザのみ、セルフサインアップを受け付ける 機能を追加したので、具体的な方法を紹介します。
やりたいこと
前提として、アプリに Amplify UI v2 の出来合いのサインイン UI が組込済みで、Amazon Cognito と連携済みです。
メールアドレスをユーザとして使用する設定にしてあります。
実現方法
Amazon Cognito の「サインアップ前 Lambdaトリガー」を使用します。
Amazon Cognito のサインアップフローの中で、サインアップ前に任意の Lambda 関数を挟み込みます。その Lambda 関数の中でメールアドレスのドメイン名をチェックし、結果を Amazon Cognito に返します。それ以外は Amazon Cognito の標準フローのままです。
設定・解説
Lambda関数
AWS公式ドキュメントにはそのまま使えるサンプルはなかったので、こちらのコードが参考になりましたら幸いです。
コード (Python)
Lambda関数のコードサンプルです。
def lambda_handler(event, context): try: print(event) # Amazon Cognitoから渡されたユーザサインアップのリクエストデータ email = event['request']['userAttributes']['email'] # リクエストデータからメールアドレスを抽出 print(email) domain = email.split('@')[1] # メールアドレスからドメイン名だけを抽出 allowedDomains = [ "example1.com", "example2.com", "example3.com" ] # サインアップを許可するドメイン名群 if domain in allowedDomains: # ユーザ入力のドメイン名が許可するドメイン名群に含まれているかチェック print('domain matched') return event # 含まれていれば、Amazon Cognito に event をそのまま返す else: print('domain unmatched') return None # 含まれていなければ、Amazon Cognito に null (PythonではNone) を返す except Exception as e: print(e)
Amazon Cognito から渡されるデータは、上記 Lambda 関数では event に格納され、以下の JSON フォーマットになっています。このデータにある限りの項目であれば Lambda 関数の中で加工・変更し Amazon Cognito に返して処理を継続させることができます。
{ "version": "1", "region": "ap-northeast-1", "userPoolId": "ap-northeast-1_XXXXXXXXX", "userName": "adea79d7-8f6f-4ff3-9552-caf0ff59239d", "callerContext": { "awsSdkVersion": "aws-sdk-unknown-unknown", "clientId": "xxxxxxxxxxxxxxxxxxxxxx" }, "triggerSource": "PreSignUp_SignUp", "request": { "userAttributes": { "email": "test@example.com" }, "validationData": null }, "response": { "autoConfirmUser": false, "autoVerifyEmail": false, "autoVerifyPhone": false } }
サインアップリクエストをそのまま進めたければ、Amazon Cognito から渡された event を加工せず return で返します。
サインアップリクエストを却下したいときには、event の代わりに null を返します。Pythonでは None と記述することに注意しましょう。
実は却下時の return での返し方がAWS公式ドキュメントに見当たりませんでした。ですが、実際に Amazon Cognito に null を返すと、以下のようにLambda関数やJSON のエラーとして画面表示され、処理が終了します。いろいろと調べて試しましたが、Amplify UI の出来合いの UI を使用する限りはエラーメッセージのカスタマイズをすることは出来なさそうです。
IAM ロール
この Lambda 関数は他のサービスを呼び出すことはないので、Lambda 関数の実行ログを Amazon CloudWatch Logs に残すための権限と、必要に応じてですが AWS X-Ray にログを残せる権限を付けておくとよいでしょう。
マネージドポリシーARN
- aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- aws:iam::aws:policy/AWSXRayDaemonWriteAccess
リソースベースポリシー
忘れてはいけないのは、この Lambda 関数が Amazon Cognito から呼び出されることを許可する権限です。
Lambda 側で以下のようなリソースベースポリシーを追加します。
詳細なパラメータはブログ最下部に CloudFormation テンプレートを紹介していますので、そちらをご覧ください。
Amazon Cognito 側 Lambda トリガー設定
マネジメントコンソールでは、以下のように「サインアップ前 Lambda トリガー」を追加します。
この連携設定をすることで、ユーザのオンラインサインアップ時、サインアップの確定前に必ず指定したLambda関数が呼び出されることになります。
CloudFormation テンプレート
以下、詳細なパラメータを含む CloudFormation テンプレートのサンプルです。
- Amazon Cognito
- ユーザプール
- アップクライアント
- メールアドレスのみをユーザ名とする
- TOTP MFA 必須
- AWS Lambda
- サインアップを許可するドメイン名は CloudFormation でパラメータ化
- リソースベースポリシー
- IAM ロール
- Lambda 関数用
AWSTemplateFormatVersion: 2010-09-09 Description: CloudFormation template example # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: DomainName: Type: String Description: Domain name for URL. xxxxx.xxx (e.g. example.com) Default: example.com MaxLength: 40 MinLength: 5 SubDomainName: Type: String Description: Sub domain name for URL. xxxx.example.com Default: subdomain MaxLength: 20 MinLength: 1 CognitoAdminEmail: Type: String Description: Cognito Admin e-mail address. (e.g. xxx@xxx.xx) Default: xxxxx@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: '"example1.com","example2.com","example3.com"' Resources: # ------------------------------------------------------------# # Lambda (triggered from Cognito) Role (IAM) # ------------------------------------------------------------# LambdaTriggeredFromCognitoRole: Type: AWS::IAM::Role Properties: RoleName: LambdaTriggeredFromCognitoRole 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 # ------------------------------------------------------------# # Cognito Lambda Invocation Permission # ------------------------------------------------------------# CognitoLambdaInvocationPermissionPresignup: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaCognitoPresignup.Arn Action: lambda:InvokeFunction Principal: cognito-idp.amazonaws.com SourceAccount: !Sub ${AWS::AccountId} SourceArn: !GetAtt UserPool.Arn DependsOn: - LambdaCognitoPresignup - UserPool # ------------------------------------------------------------# # Cognito # ------------------------------------------------------------# UserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: ExampleUserPool 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: EmailSendingAccount: DEVELOPER From: !Sub ${CognitoAdminEmail} ReplyToEmailAddress: !Sub ${CognitoAdminEmail} SourceArn: !Sub arn:aws:ses:ap-northeast-1:${AWS::AccountId}:identity/${CognitoAdminEmail} EmailVerificationMessage: Verification code: {####}" EmailVerificationSubject: Verification code" LambdaConfig: PreSignUp: !GetAtt LambdaCognitoPresignup.Arn UsernameAttributes: - email UsernameConfiguration: CaseSensitive: false UserPoolAddOns: AdvancedSecurityMode: "OFF" UserPoolClient: Type: AWS::Cognito::UserPoolClient Properties: UserPoolId: !Ref UserPool ClientName: example-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://${SubDomainName}.${DomainName}/index.html LogoutURLs: - !Sub https://${SubDomainName}.${DomainName}/index.html DefaultRedirectURI: !Sub https://${SubDomainName}.${DomainName}/index.html AllowedOAuthFlows: - implicit AllowedOAuthFlowsUserPoolClient: true AllowedOAuthScopes: - email - openid # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaCognitoPresignup: Type: AWS::Lambda::Function Properties: FunctionName: CognitoPresignup Description: Lambda Function triggered from Cognito before user self signup to check the user's email domain Runtime: python3.9 Timeout: 3 MemorySize: 128 Role: !GetAtt LambdaTriggeredFromCognitoRole.Arn Handler: index.lambda_handler Code: ZipFile: !Sub | def lambda_handler(event, context): try: print(event) email = event['request']['userAttributes']['email'] print(email) domain = email.split('@')[1] allowedDomains = [ ${AllowedUserEmailDomains} ] if domain in allowedDomains: print('domain matched') return event else: print('domain unmatched') return None except Exception as e: print(e) DependsOn: LambdaTriggeredFromCognitoRole
まとめ
細かいことを書き出すと長くなってしまうので、Amazon Cognito と AWS Lambda を中心にコード例や設定例を記載いたしました。アプリ側のことは一切記載していませんので、ある程度そこはわかっている方前提の記事となっております。ご了承ください。
この記事がみなさまのお役に立てると幸いです。