AWS CloudFormationでCognitoのサインアップ前トリガーを実施

# はじめに
今回はCognitoでのサインアップ前LambdaトリガーをAWS CloudFormationを用いて構築していきたいと思います。
これにより、Cognitoで登録されるアカウントを制限することが出来たりすることが可能になります。

# 目次
・構成について
・Lambdaの設定項目
・LambdaのCloudFormation作成
・Cognitoの設定項目
・CognitoのCloudFormation作成
・事後確認
・最後に
・参考

# 構成について
CognitoとLambdaのみで構成します。
Lambdaのコードを呼び出すのにS3を利用しますが、特に今回作成する構成に影響は出ないです。
CloudFormationの構成として、初めにLambdaを構築し、その後にCognitoを構築します。

# Lambdaの設定項目
今回はnodejs.18.xを利用し、コードについてはS3から取得してくるようにします。
IAMロールは作成しますが、Cognitoへのアクセスを許可するためのポリシーステートメントはCognitoのIDが必要なためCognitoのCloudFormationと同時に作成します。

index.jsの内容は記載しておりますが、node_modulesなどは別途対応をお願いします。
サインアップ前のチェックとして、Gmailのみ許可するという設定をしています。


上記以外の項目についてはCloudFormationの設定を見てご確認ください
また、コードを取得してくるS3については前回の記事を参考に作成してください

https://qiita.com/ogataro/items/71dd28babfcdf6410a3c

# LambdaのCloudFormation作成
LambdaとIAMロールを作成します
### Lambda_CloudFormation
```yaml:Lambda.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  Lambda Deployment

#------------------------------------------------------------#
#Parameters
#------------------------------------------------------------#
Parameters:
  PJPrefix:
    Description: Project Name
    Type: String
    Default: pj-ogataro
    AllowedPattern: ^[a-zA-Z0-9\-]*$
  Environment:
    Description: Environment Name
    Type: String
    Default: dev
    AllowedPattern: ^[a-zA-Z0-9\-]*$

#------------------------------------------------------------#
#Resources
#------------------------------------------------------------#
Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${PJPrefix}-${Environment}-Lambda'
      Runtime: nodejs18.x
      Handler: index.handler
      Code:
        S3Bucket: !Sub '${PJPrefix}-${Environment}-storage'
        S3Key: lambda/2_lambda_signin.zip
      Role: !GetAtt LambdaExecutionRole.Arn

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${PJPrefix}-${Environment}-Lambda-Singin-IAMRole'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
        - PolicyName: LambdaFunctionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:GetFunction
                Resource: !Sub 'arn:aws:lambda:ap-northeast-1:${AWS::AccountId}:function:${PJPrefix}-${Environment}-Lambda'
#------------------------------------------------------------#
#Outputs
#------------------------------------------------------------#
Outputs:
  LambdaFunction:
    Value: !Ref LambdaFunction
    Export:
      Name: !Sub '${PJPrefix}-${Environment}-Lambda-Singin-Output'
```

### Lambda_index.js
```js:index.js
exports.handler = async (event, context) => {
    console.log(event);
  
    try {
      // Amazon Cognitoから渡されたユーザサインアップのリクエストデータ
      // ユーザ登録のトリガーとなったソース情報を取得
      const triggersource = event.triggerSource;
      // リクエストデータからメールアドレスを抽出
      const email = event.request.userAttributes.email;
      console.log(email);
      if (triggersource === 'PreSignUp_SignUp') {
        // トリガーソースがセルフサインアップの場合、ドメイン名チェックを実行
        console.log('via self signup');
        const domain = email.split('@')[1];
        // サインアップを許可するドメイン名群
        const allowedDomains = [
          'gmail.com'
        ];
        // 許可ドメインに含まれている場合
        if (allowedDomains.includes(domain)) {
          console.log('domain matched');
          return event;
        } else {
          console.log('domain unmatched');
          return null;
        }
      } else {
        // トリガーソースがセルフサインアップ以外の場合、ドメイン名チェックを実行しない
        console.log('via admin console');
        return event;
      }
    } catch (error) {
      console.log(error);
    }
  };
```

# Cognitoの設定項目
サインアップ前Lambdaトリガー以外の設定については以前の記事と同じです。
サインアップ前にユーザー登録するメールアドレスドメインをチェックするLambdaを呼び出します。

https://qiita.com/ogataro/items/84b18ecfc293f947ad23


# CognitoのCloudFormation作成
Cognitoのユーザープールとそれに関係するユーザープールドメインやユーザープールクライアント、Lambdaのポリシーステートメントを作成します
```yaml:Cognito.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: >
  CognitoUserPool deployment

#------------------------------------------------------------#
#Parameters
#------------------------------------------------------------#
Parameters:
  PJPrefix:
    Description: Project Name
    Type: String
    Default: pj-ogataro
    AllowedPattern: ^[a-zA-Z0-9\-]*$
  Environment:
    Description: Environment Name
    Type: String
    Default: dev
    AllowedPattern: ^[a-zA-Z0-9\-]*$


#------------------------------------------------------------#
#Resources
#------------------------------------------------------------#
Resources:
  ##ユーザープール作成
  UserPool:
    Type: AWS::Cognito::UserPool 
    Properties:
      UserPoolName: !Sub '${PJPrefix}-${Environment}-UserPool'
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: False
      UsernameAttributes:
        - email # Eメールでのサインインを有効にする場合は"email"を指定
      AccountRecoverySetting:
        RecoveryMechanisms:
          - Name: verified_email
            Priority: 1
      MfaConfiguration: 'ON'
      EnabledMfas:
        - SOFTWARE_TOKEN_MFA #アプリケーションでのMFA
      AutoVerifiedAttributes:
        - email
      LambdaConfig: #サインアップ前Lambdaトリガー
        PreSignUp: !Sub 'arn:aws:lambda:ap-northeast-1:${AWS::AccountId}:function:${PJPrefix}-${Environment}-Lambda' 
    UpdateReplacePolicy: Retain
    DeletionPolicy: Delete

  #ユーザープールドメイン作成
  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Sub '${PJPrefix}-${Environment}-domain' 
      UserPoolId: !Ref UserPool

  #アプリケーション統合作成
  AppIntegration:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      CallbackURLs: 
        - https://www.yahoo.co.jp/
      ClientName: !Sub '${PJPrefix}-${Environment}-cognito-app'
      UserPoolId: !Ref UserPool
      RefreshTokenValidity: 1
      SupportedIdentityProviders:
        - COGNITO
      AllowedOAuthFlows:
        - code
      AllowedOAuthScopes:
        - openid

  LambdaPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      FunctionName: 
        Fn::ImportValue:
          !Sub '${PJPrefix}-${Environment}-Lambda-Singin-Output'
      Action: "lambda:InvokeFunction"
      Principal: "cognito-idp.amazonaws.com"
      SourceArn: !GetAtt UserPool.Arn
```

# 事後確認
正常に構築ができたか確認していきます。
Cognitoのアプリケーション統合から「ホストされたUIを表示」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513082/4c727aa1-a3c0-43ae-0452-a3a54e68e58f.png)

そこでGmailでのみサインアップでき、Gmail以外の場合はエラーとなることを確認してください。
Gmail以外でサインアップしようとした場合、【Invalid lambda function output : Invalid JSON】と表示されれば完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513082/350d68d1-04da-da92-d2c2-d19ff7d4fb4f.png)

不具合などがあった場合、Lambdaトリガーの紐づけがきちんと行われていない場合があるので、
CognitoのユーザープールのプロパティにてLambdaトリガーの更新をしてから保存を実施してみてください。
不具合が解消される場合があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513082/f90bf212-0c7b-2a05-18f4-bd0fe7969bee.png)


# 最後に
こちらでCognitoのサインアップ前トリガーを導入する手順は完了です。
参考になれば、幸いです。

# 参考サイト
Lambda

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html

Cognito

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html