Amazon Cognitoでセルフサインアップ時にメールアドレスをチェックする方法 [Amplify + Cognito + Lambda]

こんにちは、広野です。

先日、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 を中心にコード例や設定例を記載いたしました。アプリ側のことは一切記載していませんので、ある程度そこはわかっている方前提の記事となっております。ご了承ください。

この記事がみなさまのお役に立てると幸いです。

著者について
広野 祐司

AWSサーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリや教育コンテンツをつくっています。ReactでSPAを書き始めたら、快適すぎて他の開発言語には戻れなくなりました。AWSサーバーレスやReactの仲間を増やしたいです。
取得資格:AWS認定は7つ、ITサービスマネージャ、ITIL v3 Expert、等
2020, 2021 APN AWS Top Engineers 受賞
2022 AWS Partner Ambassador 受賞
好きなAWSサービス:AWS Amplify / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする
クラウドに強いによるエンジニアブログです。
SCSKは専門性と豊富な実績を活かしたクラウドサービス USiZE(ユーサイズ)を提供しています。
USiZEサービスサイトでは、お客様のDX推進をワンストップで支援するサービスの詳細や導入事例を紹介しています。
AWSサーバレスアーキテクチャ
TechHarmony
タイトルとURLをコピーしました