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

こんにちは、広野です。

以前、以下の記事で Amazon Cognito のセルフサインアップ時にメールアドレスのドメイン名をチェックする方法を紹介しました。

実はセルフサインアップ時にと言いつつも、管理者が AWS マネジメントコンソールからユーザ登録するときもセルフサインアップと同様にチェックが走ってしまいます。ここでは、例外として管理者によるユーザ登録はドメイン名チェック無しにするように変更をかける、という続編記事を書きたいと思います。

本記事をお読みになる前に、前回記事をお読み頂くことをお勧めします。

やりたいこと

  • ユーザのセルフサインアップ時のみ、メールアドレスのドメイン名をチェックするようにする。
  • 管理者が AWS マネジメントコンソールからユーザ登録するときは、任意のドメイン名のメールアドレスを登録可能とする。

実現方法

  • ドメイン名チェックを行う Lambda 関数に条件分岐を入れる。セルフサインアップをトリガーとしたユーザ登録の場合のみドメイン名チェックをする。

修正した AWS Lambda 関数

def lambda_handler(event, context):
  try:
    print(event) # Amazon Cognitoから渡されたユーザサインアップのリクエストデータ
    triggersource = event['triggerSource'] # ユーザ登録のトリガーとなったソース情報を取得
    email = event['request']['userAttributes']['email'] # リクエストデータからメールアドレスを抽出
    print(email)
    if triggersource == 'PreSignUp_SignUp': # トリガーソースがセルフサインアップだったら、ドメイン名チェックを実行
      print('via self signup')
      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) を返す
    else: # トリガーソースがセルフサインアップ以外であれば、ドメイン名チェックを実行しない
      print('via admin console')
      return event
  except Exception as e:
    print(e)

Amazon Cognito からユーザ登録情報が AWS Lambda 関数に渡されるときに、triggerSource というキー名でトリガーがセルフサインアップなのか、マネジメントコンソールからの登録なのかを判別できます。

私が試した限り、以下の triggerSource がわかりました。

ケース triggerSource の値 備考
セルフサインアップ PreSignUp_SignUp  
MC からのユーザ登録 PreSignUp_AdminCreateUser Management Console から手動でユーザ登録したケース。
API によるユーザ登録 PreSignUp_AdminCreateUser Lambda 関数から SDK でユーザ登録したケース。AWS CLI は未検証。

これで目的は果たせました。以下、AWS CloudFormation テンプレートは修正後のものを貼り付けておきます。

AWS CloudFormation テンプレート

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates sample resources for checking domain name of user email addresses with Cognito and Lambda.

# ------------------------------------------------------------#
# 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) # Amazon Cognitoから渡されたユーザサインアップのリクエストデータ
              triggersource = event['triggerSource'] # ユーザ登録のトリガーとなったソース情報を取得
              email = event['request']['userAttributes']['email'] # リクエストデータからメールアドレスを抽出
              print(email)
              if triggersource == 'PreSignUp_SignUp': # トリガーソースがセルフサインアップだったら、ドメイン名チェックを実行
                print('via self signup')
                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) を返す
              else: # トリガーソースがセルフサインアップ以外であれば、ドメイン名チェックを実行しない
                print('via admin console')
                return event
            except Exception as e:
              print(e)
    DependsOn: LambdaTriggeredFromCognitoRole

おまけ

今回、ユーザ登録時のドメイン名チェック実行有無を条件分岐させましたが、以下のユーザ登録フローにあるメールアドレス存在確認も管理者によるユーザ登録のときに意図的にスキップすることができます。

メールアドレスを最初から「検証済み」にマークしておけば存在確認のステップは実行されません。

実運用では、要件によってこれらの手法を選別して頂けたらと思います。

まとめ

いかがでしたでしょうか?

Amazon Cognito はまだまだ奥が深いです。引き続きいろいろな使い方を試していきたいと思います。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

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をコピーしました