LoginSignup
9
10

More than 5 years have passed since last update.

AWS CloudFormationでCognitoユーザープールをMFAのTOTPを有効にして作成する

Posted at

概要

Cognitoのユーザープールを作成するのに、AWS マネジメントコンソールを利用するのが面倒になり、AWS SDK for Pythonを利用して面倒さを解消したのですが、AWSにはCloudFormation(CFn)という素敵サービスがありますので、それを利用してさらに手間を省けないか検証してみました。

CFnでCognitoはすでに対応されていますが、MFA有効化してTOTPを選択するのに少し手間がかかりました。そのうちにもっと簡単に作成されるといいですね。(2018/11/09時点)

CloudFormation で Cognito
https://qiita.com/y13i/items/1923b47079bdf7c44eec

Amazon Cognito Now Supported by AWS CloudFormation
https://aws.amazon.com/jp/about-aws/whats-new/2017/04/amazon-cognito-now-supported-by-aws-cloudformation/

CognitoやMFA、TOTPってなんぞ?という方は下記をご参考ください。

Amazon Cognitoのワンタイムパスワード(TOTP)認証をNode.jsで試してみた
https://qiita.com/kai_kou/items/aa7dc0042b44649f576a

PythonでAmazon Cognitoのユーザープールを作成してみる
https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe

AWS CloudFormation(Cfn)とは

AWS CloudFormation (設定管理とオーケストレーション) | AWS
https://www.google.co.jp/search?q=CloudFormation

AWS CloudFormation は、クラウド環境内のすべてのインフラストラクチャリソースを記述してプロビジョニングするための共通言語を提供します。CloudFormation では、シンプルなテキストファイルを使用して、あらゆるリージョンとアカウントでアプリケーションに必要とされるすべてのリソースを、自動化された安全な方法でモデル化し、プロビジョニングできます。このファイルは、クラウド環境における真の単一ソースとして機能します。

CloudFormation超入門
https://dev.classmethod.jp/beginners/chonyumon-cloudformation/

誤解を恐れつつ一言で言えば、「自動的にAWS上で作りたいものを作ってくれる」サービスです。というか、そういう環境を用意してくれるサービスです。

CFnのテンプレート

今回作成したCFnのテンプレートです。

CFnのテンプレートはAWS マネジメントコンソールにあるデザイナーでも作成できますが、今回は利用せずに作成しました。フォーマットはJSON、YAMLが利用できますが、YAMLで作成しています。

検証用のユーザーをあわせて作成していますが、ユーザー名、パスワードはべた書きなので、あしからず。

ソースはGitHubにもアップしています。
https://github.com/kai-kou/create-cognito-user-pool-at-cloudformation

create-user-pool-template.yaml
Resources:
  # ユーザープールの作成
  UserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
      UserPoolName:
        Ref: AWS::StackName
      MfaConfiguration: 'OFF'
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7

  # ユーザープールにアプリクライアントを作成
  UserPoolClient:
    Type: "AWS::Cognito::UserPoolClient"
    Properties:
      UserPoolId:
        Ref: UserPool
      ClientName:
        Ref: AWS::StackName
      RefreshTokenValidity: 30

  # ユーザープールでMFA(TOTP)有効化
  UserPoolMfaConfig:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoSetUserPoolMfaConfigFunction.Arn
      UserPoolId:
        Ref: UserPool

  # 検証用のユーザーを追加
  AdminCreateUser:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoAdminCreateUserFunction.Arn
      UserPoolId:
        Ref: UserPool
      UserName: hoge
      Password: hogeHoge7!

  # 検証用のユーザーを追加
  AdminCreateUser2:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoAdminCreateUserFunction.Arn
      UserPoolId:
        Ref: UserPool
      UserName: hoge2
      Password: hogeHoge7!

  # ユーザープールでMFA(TOTP)有効化するLambda関数
  CognitoSetUserPoolMfaConfigFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            # スタック削除時にも実行されるので、処理せずに終了させる
            if event['RequestType'] == 'Delete':
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            # UserPoolIDを取得する
            user_pool_id = event['ResourceProperties']['UserPoolId']
            print(f'user_pool_id: {user_pool_id}')

            # MFA有効化してTOTPを指定する
            response_data = {}
            try:
              client = boto3.client('cognito-idp')
              response_data = client.set_user_pool_mfa_config(
                UserPoolId=user_pool_id,
                SoftwareTokenMfaConfiguration={
                  'Enabled': True
                },
                MfaConfiguration='ON'
              )

            except Exception as e:
              print("error: " + str(e))
              response_data = {'error': str(e)}
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
              return

            print(response_data)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Runtime: python3.6

  # ユーザープールにテスト用ユーザーを作成するLambda関数
  CognitoAdminCreateUserFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            print(event['RequestType'])
            # スタック削除時にも実行されるので、処理せずに終了させる
            if event['RequestType'] == 'Delete':
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            # UserPoolIDを取得する
            user_pool_id = event['ResourceProperties']['UserPoolId']
            print(f'user_pool_id: {user_pool_id}')

            # ユーザー名、パスワードを取得する
            username = event['ResourceProperties']['UserName']
            password = event['ResourceProperties']['Password']

            # 検証用のユーザーを作成する
            response_data = {}
            try:
              client = boto3.client('cognito-idp')
              response_data = client.admin_create_user(
                UserPoolId=user_pool_id,
                Username=username,
                TemporaryPassword=password,
                MessageAction='SUPPRESS'
              )

              # Datetime型のままなので文字列に変換する
              response_data['User']['UserCreateDate'] = \
                response_data['User']['UserCreateDate'].strftime('%c')
              response_data['User']['UserLastModifiedDate'] = \
                response_data['User']['UserLastModifiedDate'].strftime('%c')

            except Exception as e:
              print("error: " + str(e))
              response_data = {'error': str(e)}
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
              return

            print(response_data)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Runtime: python3.6

  # Lambda関数実行用のロール
  CognitoFunctionExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: "arn:aws:logs:*:*:*"
          # Cognitoの操作権限を付与する
          - Effect: Allow
            Action:
              - cognito-idp:*
            Resource: "arn:aws:cognito-idp:*:*:userpool/*"

はい。
ざっくりとポイントだけ。

ユーザープール作成時にTOTPを利用したMFA有効化はできない

MfaConfiguration というパラメータでMFA有効化できるのですが、いまのところ有効化時にTOTPを指定するパラメータがありませんでしたので、とりあえず、OFF で作成しています。(2018/11/09時点)

パラメータについては公式のドキュメントが参考になります。

AWS::Cognito::UserPool - AWS CloudFormation
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html

ユーザープール名UserPoolName の指定はRef: AWS::StackName とすることでCFnのスタック名としています。

  UserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
      UserPoolName:
        Ref: AWS::StackName
      MfaConfiguration: 'OFF'
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7

AWS Lambda-backed カスタムリソースを利用してMFAを有効化する

CFnにはカスタムリソースというものが用意されており、CFnが対応していないリソースを自前で管理することができます。さらにカスタムリソースを定義する際にLambda関数を利用することができます。便利ですね^^

AWS Lambda-backed カスタムリソース - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

Lambda 関数とカスタム リソースを関連付けた場合、この関数はカスタム リソースが作成、更新、または削除されるたびに呼び出されます。

こちらを利用して、CognitoのユーザープールでMFA有効化します。

カスタムリソースの定義

  UserPoolMfaConfig:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoSetUserPoolMfaConfigFunction.Arn
      UserPoolId:
        Ref: UserPool

UserPoolMfaConfig はカスタムリソースの名称となり任意で指定できます。Type: Custom::CustomResource とすることでカスタムリソースと定義します。
ServiceToken に実行するLambda関数のArnを指定、UserPoolId はLambda関数に引き渡すパラメータになります。ここではRef: UserPool として、作成されたユーザープールのIDを渡しています。

Lambda関数の定義

  CognitoSetUserPoolMfaConfigFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          (略)
      Runtime: python3.6

実行するLambda関数は作成済みの関数も指定できますが、同一テンプレート(スタック)内で作成することもできます。(すごい!便利!

今回はAWS SDKを利用するだけでよかったので、ソースも含めて定義していますが、もし他のライブラリをインポートしたい場合は、ソースをZIP圧縮してS3へアップしたものを指定することができます。通常のAWS Lambda関数をデプロイする方法と同じです。

Lambda関数の実行に必要なロールも同一テンプレート(スタック)内で作成して指定ができます。

Handler: index.handler としていますが、index に関してはFCnで関数を作成する場合、ファイル名がindex.py となるので、変更不可のようです。

Lambda関数の詳細については下記が参考になります。

AWS Lambda 関数コード - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

Lamnda関数の実装

import cfnresponse
import boto3
def handler(event, context):
  # スタック削除時にも実行されるので、処理せずに終了させる
  if event['RequestType'] == 'Delete':
    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
    return

  # UserPoolIDを取得する
  user_pool_id = event['ResourceProperties']['UserPoolId']
  print(f'user_pool_id: {user_pool_id}')

  # MFA有効化してTOTPを指定する
  response_data = {}
  try:
    client = boto3.client('cognito-idp')
    response_data = client.set_user_pool_mfa_config(
      UserPoolId=user_pool_id,
      SoftwareTokenMfaConfiguration={
        'Enabled': True
      },
      MfaConfiguration='ON'
    )

  except Exception as e:
    print("error: " + str(e))
    response_data = {'error': str(e)}
    cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
    return

  print(response_data)
  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

Lambda関数はCFnのスタック作成時だけでなく、更新や削除時にも実行されます。
今回は削除するケースに対応するのに、if event['RequestType'] == 'Delete': で、削除時には処理をスキップするようにしています。

例外のハンドリングなど他にも考慮すべき点がありますが、下記が参考になります。

CloudFormationでLambdaを実行する
https://prgrmmbl.com/2018/07/01/Lambda-With-CloudFormation.html

Lambda-backed Custom Resourceのcfn-responseモジュールを利用する上での注意点
https://dev.classmethod.jp/cloud/aws/note-about-using-delete-response-with-cfn-response-module-in-lambda-backed-custom-resource/

Cognitoのユーザープールの操作にはPythonで提供されているAWS SDKのboto3 を利用しています。SDKの利用に関しては下記をご参考ください。

PythonでAmazon Cognitoのユーザープールを作成してみる
https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe

作成する

CFnで上記テンプレートからスタックを作成してみます。

AWS CLIで作成する

AWS CLIを利用して、CFnのスタックを作成してみます。
上記のテンプレートを適当なディレクトリに保存して実行します。

AWS CLIのインストールについては下記が参考になります。

AWS CLIのインストール
https://qiita.com/yuyj109/items/3163a84480da4c8f402c

> aws --version
aws-cli/1.16.27 Python/3.6.6 Darwin/17.7.0 botocore/1.12.17

> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> touch create-user-pool-template.yaml
> vi create-user-pool-template.yaml

create-stack - AWS CLI 1.16.51 Command Reference
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html

AWS CloudformationをAWS CLIから使ってみる
https://techte.co/2018/01/25/cloudformation/

> aws cloudformation create-stack \
  --region ap-northeast-1 \
  --stack-name cognito-totp-mfa-user-pool \
  --template-body file://create-user-pool-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxx:stack/cognito-totp-mfa-user-pool/3d3591e0-e3ca-11e8-bdc2-503a6ff78e2a"
}

create-stack でスタックを作成します。
ロールを作成する場合、--capabilities CAPABILITY_IAM と指定しておかないと、Requires capabilities : [CAPABILITY_IAM] と怒られるので、ご注意ください。

Requires capabilities : [CAPABILITY_IAM]
https://github.com/awslabs/serverless-application-model/issues/51

describe-stacks でスタックの情報が確認できます。

> aws cloudformation describe-stacks \
  --stack-name cognito-totp-mfa-user-pool

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxx:stack/cognito-totp-mfa-user-pool/752fbbe0-e3d2-11e8-8ab5-50a686699882",
            "StackName": "cognito-totp-mfa-user-pool",
            "CreationTime": "2018-11-09T03:49:28.365Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_IN_PROGRESS",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Tags": [],
            "EnableTerminationProtection": false
        }
    ]
}

AWS マネジメントコンソールでもスタックが作成ことが確認できます。
CloudFormation_-_Stack-2.png
CloudFormation_-_Stack_cognito-totp-mfa-user-pool.png

AWS マネジメントコンソールで作成する

最近CloudFormationの操作UIが新しくなりプレビュー版が利用できるようになったみたいです。LambdaのUIっぽくて良い感じです。せっかくなので新しいUIで試してみます。

CloudFormation_-_Stack-3.png
ローカルからファイルを指定するとS3にアップロードされます。
CloudFormation_-_Stack_create.png
CloudFormation_-_Stack_create-2.png
スクリーンショット 2018-11-09 12.07.44.png
スクリーンショット_2018-11-09_12_07_53.png
スクリーンショット 2018-11-09 12.08.29.png
最後に、テンプレート内でロール作成の定義があるから気をつけてねと確認がでてきます。
AWS CLIで作成したときの--capabilities CAPABILITY_IAM と同じ確認ですね。
スクリーンショット_2018-11-09_12_08_38.png

ユーザープールの確認

Cognitoでユーザープールが作成されたか確認してみます。
ユーザープール_-_Amazon_Cognito-2.png
スクリーンショット 2018-11-09 12.10.54.png
スクリーンショット 2018-11-09 12.11.12.png
はい。
ちゃんとMFA有効化されて、ユーザーも作成されています。

やったぜ。

最後に、スタックを削除して、関連するリソースが削除されることを確認しておきます。
注意点としては、テンプレートで定義した各リソースは削除されますが、S3にアップロードされたテンプレートファイルや、Lambda関数のログは残ったままになりますので、そちらは手動で削除することになります。(未確認)

> aws cloudformation delete-stack \
  --stack-name cognito-totp-mfa-user-pool

> aws cloudformation describe-stacks \
  --stack-name cognito-totp-mfa-user-pool

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id cognito-totp-mfa-user-pool does not exist

まとめ

CFnを利用することで、AWS マネジメントコンソールやスクリプトを書くことなくAWSのリソースを作成・管理できることがわかりました。カスタムリソースを利用することで、より柔軟なリソース管理もできるので、応用がとても効きます。

いままで使ったことがなかったのですが、環境構築の再現性も非常に高いので、AWSを利用するのに必須といっていいかもしれません。

参考

CloudFormation で Cognito
https://qiita.com/y13i/items/1923b47079bdf7c44eec

Amazon Cognito Now Supported by AWS CloudFormation
https://aws.amazon.com/jp/about-aws/whats-new/2017/04/amazon-cognito-now-supported-by-aws-cloudformation/

Amazon Cognito Resource Types Reference
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-reference-cognito.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

Amazon Cognitoのワンタイムパスワード(TOTP)認証をNode.jsで試してみた
https://qiita.com/kai_kou/items/aa7dc0042b44649f576a

PythonでAmazon Cognitoのユーザープールを作成してみる
https://qiita.com/kai_kou/items/f1623ac1f3d7aaab23fe

AWS CloudFormation (設定管理とオーケストレーション) | AWS
https://www.google.co.jp/search?q=CloudFormation

CloudFormation超入門
https://dev.classmethod.jp/beginners/chonyumon-cloudformation/

AWS::Cognito::UserPool - AWS CloudFormation
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html

AWS Lambda-backed カスタムリソース - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

AWS Lambda 関数コード - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

CloudFormationでLambdaを実行する
https://prgrmmbl.com/2018/07/01/Lambda-With-CloudFormation.html

Lambda-backed Custom Resourceのcfn-responseモジュールを利用する上での注意点
https://dev.classmethod.jp/cloud/aws/note-about-using-delete-response-with-cfn-response-module-in-lambda-backed-custom-resource/

AWS CLIのインストール
https://qiita.com/yuyj109/items/3163a84480da4c8f402c

Requires capabilities : [CAPABILITY_IAM]
https://github.com/awslabs/serverless-application-model/issues/51

create-stack - AWS CLI 1.16.51 Command Reference
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10