Help us understand the problem. What is going on with this article?

Serverless Framework で Cognito を構築する

この記事は CodeChrysalis Advent Calendar 2019 の記事です。

Serverless FrameworkでDynamoDBを構築することについての記事もぜひ御覧ください。

はじめに

Serverless Framework はYAMLでサーバサイドの環境構築ができる、Infrastructure As Code の真骨頂です。確かにAWS等のコンソールでもポチポチクリックしながら環境構築はできますが、

  1. 変更の管理ができない
  2. 画面でポチポチクリックするのが本当に面倒(Lazy...)

なのでServerless Framework の CLI でコマンド一発で全てが構築されるのはとても気持ちが良い。今回はその中でもAWSのCognitoをどのように構築するかを紹介します。

この説明の前提は、認証プロバイダはCognito Identity Poolとし、Cognito User Poolと連携して、API GatewayにアクセスさせるときにデフォルトのAWS authorizerを使用したいという状況とします。

まずはYAML

早速YAMLを紹介します。

serverless.yml

service: cc-cognito

custom:
  stage: ${opt:stage, self:provider.stage}

provider:
  name: aws
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: Allow
      Action:
        - cognito-idp:ListUsers
        - cognito-idp:AdminListGroupsForUser
      Resource: "arn:aws:cognito-idp:ap-northeast-1:*:*"

resources:
  # IAM
  - ${file(resources/iam-roles.yml)}
  # Cognito
  - ${file(resources/cognito-user-pool.yml)}
  - ${file(resources/cognito-identity-pool.yml)}

cognito-user-pool.yml

Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AutoVerifiedAttributes:
        - email
      EmailConfiguration:
        EmailSendingAccount: "DEVELOPER"
        SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"
      EmailVerificationMessage: "任意のEmail本文"
      EmailVerificationSubject: "任意のEmailタイトル"
      MfaConfiguration: "OFF"
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
      UserPoolName: ${self:custom.stage}-CognitoUserPool
      UsernameAttributes:
        - email

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:custom.stage}-CognitoUserPoolClient
      ExplicitAuthFlows:
        - ADMIN_USER_PASSWORD_AUTH
      GenerateSecret: false
      UserPoolId:
        Ref: CognitoUserPool

Outputs:
  UserPoolId:
    Value:
      Ref: CognitoUserPool
    Export:
      Name: ${self:custom.stage}-UserPoolId

  UserPoolClientId:
    Value:
      Ref: CognitoUserPoolClient
    Export:
      Name: ${self:custom.stage}-UserPoolClientId

cognito-identity-pool.yml

Resources:
  CognitoUserIdentityPool:
    Type: AWS::Cognito::IdentityPool
    Properties:
      IdentityPoolName: ${self:custom.stage}CognitoUserIdentityPool
      AllowUnauthenticatedIdentities: false
      CognitoIdentityProviders:
        - ClientId:
            Ref: CognitoUserPoolClient
          ProviderName:
            Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ]

  CognitoUserIdentityPoolRoleAttachment:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId:
        Ref: CognitoUserIdentityPool
      Roles:
        authenticated:
          Fn::GetAtt: [CognitoUserAuthRole, Arn]

Outputs:
  CognitoUserIdentityPool:
    Value:
      Ref: CognitoUserIdentityPool
    Export:
      Name: ${self:custom.stage}-CognitoUserIdentityPool

iam-roles.yml

Resources:
  CognitoUserAuthRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Federated: "cognito-identity.amazonaws.com"
            Action:
              - "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud":
                  Ref: CognitoUserIdentityPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": authenticated
      Policies:
        - PolicyName: "CognitoAuthorizedPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "mobileanalytics:PutEvents"
                  - "cognito-sync:*"
                  - "cognito-identity:*"
                Resource: "*"
              - Effect: "Allow"
                Action:
                  - "execute-api:Invoke"
                Resource:
                  # Allow users to invoke hello GET API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/GET/hello"
                  # Allow users to invoke hello POST API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/POST/hello"

関係性を解説

Cognito User Pool

ユーザーID(Emailや電話番号とすることもできる)とパスワードを保管して、サインアップやログイン等を司ることができます。MFA(多要素認証)も使うことができます。

ログインに成功すると、何に対してアクセスできるのかを後述するCognito Identity Poolに判断させます。

Cognito Identity Provider

今回でいうとIdentityを提供してくれる、すなわちCognito Identity ProviderであるCognito User Poolから提供された情報に基づき、どのIAMロールを付加するかを判断します。

これによって例えばログインしてきたユーザーはどのエンドポイントにアクセスできるかを切り分けることができます。管理者とユーザーそれぞれでユーザープールを作り、それぞれで異なるIAMロールを割り当てることで実現できます。

IAM

IAMはどんなAWS製品でも避けては通れない認識なので皆さんご存知なのではないでしょうか。権限を付与するためのものです。今回でいうと、Cognito Identity Poolによってログインしてきたユーザーに割り当てるIAMを作ります。

YAMLの要素を解説

serverless.yml

Serverless Framework で DynamoDB を構築する
の続きで、マイクロサービスを前提としますので、まずはserverless.ymlです。

再掲

service: cc-auth

custom:
  stage: ${opt:stage, self:provider.stage}

provider:
  name: aws
  stage: dev
  profile: cc-admin
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: Allow
      Action:
        - cognito-idp:ListUsers
        - cognito-idp:AdminListGroupsForUser
      Resource: "arn:aws:cognito-idp:ap-northeast-1:*:*"

resources:
  # IAM
  - ${file(resources/iam-roles.yml)}
  # Cognito
  - ${file(resources/cognito-user-pool.yml)}
  - ${file(resources/cognito-identity-pool.yml)}

ここでresourcesに渡したのは3つのファイル参照です。Cognitoは認証を司るため、認証まわりを一つのサービスと見立ててマイクロサービス化しています。逆にCognito User Pool, Cognito Identity Pool, IAMは認証の部品なので一つのマイクロサービスに包みました。が、それぞれ別の製品なので別のファイルで管理するようにしました。

他のプロパティについてはServerless Framework で DynamoDB を構築する
を参照してください。

Cognito User Pool

再掲

Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AutoVerifiedAttributes:
        - email
      EmailConfiguration:
        EmailSendingAccount: "DEVELOPER"
        SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"
      EmailVerificationMessage: "任意のEmail本文"
      EmailVerificationSubject: "任意のEmailタイトル"
      MfaConfiguration: "OFF"
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
      UserPoolName: ${self:custom.stage}-CognitoUserPool
      UsernameAttributes:
        - email

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:custom.stage}-CognitoUserPoolClient
      ExplicitAuthFlows:
        - ADMIN_USER_PASSWORD_AUTH
      GenerateSecret: false
      UserPoolId:
        Ref: CognitoUserPool

Outputs:
  UserPoolId:
    Value:
      Ref: CognitoUserPool
    Export:
      Name: ${self:custom.stage}-UserPoolId

  UserPoolClientId:
    Value:
      Ref: CognitoUserPoolClient
    Export:
      Name: ${self:custom.stage}-UserPoolClientId

基本的にはAWS CloudFormationの文法に基づきます。

Type: AWS::Cognito::UserPool でこれからCognito User Poolを作るという宣言になります。

AutoVerifiedAttributesはユーザーがサインアップするときに何を以て確認するかです。選択肢はEmailかSMSがあります。今回はEmailを選択しました。

      EmailConfiguration:
        EmailSendingAccount: "DEVELOPER"
        SourceArn: "arn:aws:ses:[ここにArn]:identity/[ここにemailAddress]"

ここでは任意のEmailアドレスから確認のEmailを送信したいときの設定をしています。AWSのデフォルトのEmailアドレスから送信するのでよければこの設定は不要です。

もしこの機能を利用する場合は、事前にAWS SESで任意のEmailアドレスを登録し、サンドボックスを解除する必要があります。

EmailVerificationMessageは任意のEmail本文を使うための設定、EmailVerificationSubjectは任意のタイトルを使うための設定です。

MfaConfigurationは多要素認証を利用するかどうかです。今回はOFFにしています。

      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true

これはパスワードの強固さの規定です。上から順番に、最低何文字か、小文字を必要とするか、数字を必要とするか、記号を必要とするか、大文字を必要とするかの設定です。

UserPoolNameはユーザープールの名前を何にするかです。Serverless Framework で DynamoDB を構築する
でご紹介したcustomを用いることにより、開発環境と本番環境の名前を分けています。

      UsernameAttributes:
        - email

これはCognitoにどんなデータをもたせるかです。実はCognitoもデータベースのようにいろいろな情報をもたせることができます。が、Cognitoに持たせるかデータベースに持たせるかは設計次第です。

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ${self:custom.stage}-CognitoUserPoolClient
      ExplicitAuthFlows:
        - ADMIN_USER_PASSWORD_AUTH
      UserPoolId:
        Ref: CognitoUserPool

Cognito User Pool Clientの設定です。ClientNameは任意の名前を設定します。ここでもcustomプロパティを使って本番環境と開発環境で名前を区別しています。

ExplicitAuthFlowsはCognito Clientでサポートする認証フローです。設定したい認証フローに応じて値を変えます。ドキュメントはこちらにあります。

UserPoolId はどのPoolに帰属するClientかを設定します。Ref関数を使って先ほど設定したPoolと関連付けました。

Cognito Identity Pool

再掲

Resources:
  CognitoUserIdentityPool:
    Type: AWS::Cognito::IdentityPool
    Properties:
      IdentityPoolName: ${self:custom.stage}CognitoUserIdentityPool
      AllowUnauthenticatedIdentities: false
      CognitoIdentityProviders:
        - ClientId:
            Ref: CognitoUserPoolClient
          ProviderName:
            Fn::GetAtt: [ "CognitoUserPool", "ProviderName" ]

  CognitoUserIdentityPoolRoleAttachment:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId:
        Ref: CognitoUserIdentityPool
      Roles:
        authenticated:
          Fn::GetAtt: [CognitoUserAuthRole, Arn]

Outputs:
  CognitoUserIdentityPool:
    Value:
      Ref: CognitoUserIdentityPool
    Export:
      Name: ${self:custom.stage}-CognitoUserIdentityPool

Cognito Identity Poolを作成するときのTypeはAWS::Cognito::IdentityPoolです。IdentityPoolNameは名前なので任意の名前とします。

AllowUnauthenticatedIdentitiesは認証されていないユーザーを許可するかどうかですが、サインインしないとリソースにアクセスさせない場合はfalseとします。

CognitoIdentityProvidersは認証情報は何が提供してくれるのかを定義するところです。ClientIdはRef関数でCognitoUserPoolClientを、ProviderNameFn::GetAttCognitoUserPoolを参照していますが、これはcognito-user-pool.ymlでOutputsを設定しているからです。Outputsに設定したものは同じアカウントのCloudFormation上で作成されたものは読み取ることができます。

CognitoUserIdentityPoolRoleAttachmentでは、どのIdentity PoolでどのIAMロールを割り当てるかを決めるものです。IAMロールは後述しますが、ここではつい先程設定したCognitoUserIdentityPoolにはCognitoUserAuthRoleのロールを割り当てる設定となっています。

IAM

再掲

Resources:
  CognitoUserAuthRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Federated: "cognito-identity.amazonaws.com"
            Action:
              - "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud":
                  Ref: CognitoUserIdentityPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": authenticated
      Policies:
        - PolicyName: "CognitoAuthorizedPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "mobileanalytics:PutEvents"
                  - "cognito-sync:*"
                  - "cognito-identity:*"
                Resource: "*"
              - Effect: "Allow"
                Action:
                  - "execute-api:Invoke"
                Resource:
                  # Allow users to invoke hello GET API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/GET/hello"
                  # Allow users to invoke hello POST API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/POST/hello"

まずAssumeRolePolicyDocumentで誰がどのようにユーザーにIAMロールを割り当てるのかを設定します。PrincipalがAWSのリソース(この例ではPoliciesに記載されているLambda)に対してアクセスするリクエストを発行する主体になります。ここではCognito Identity Poolとするため、"cognito-identity.amazonaws.com"としています。

            Action:
              - "sts:AssumeRoleWithWebIdentity"

アプリにサインアップしたユーザーにIAMロールを割り当てるためにAWS STSのsts:AssumeRoleWithWebIdentityを使います。IAMユーザーに登録しなくともAWSのリソースに一時的にアクセスすることを許すためのものです。

            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud":
                  Ref: CognitoUserIdentityPool
              "ForAnyValue:StringLike":
                "cognito-identity.amazonaws.com:amr": authenticated

これはIAMロールを割り当てるための条件です。認証を司るものがCognitoUserIdentityPoolであって、かつその結果がauthenticatedのもの、つまりログインされているものだけとしています。

      Policies:
        - PolicyName: "CognitoAuthorizedPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "mobileanalytics:PutEvents"
                  - "cognito-sync:*"
                  - "cognito-identity:*"
                Resource: "*"
              - Effect: "Allow"
                Action:
                  - "execute-api:Invoke"
                Resource:
                  # Allow users to invoke hello GET API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/GET/hello"
                  # Allow users to invoke hello POST API
                  - Fn::Join:
                      - ""
                      - - "arn:aws:execute-api:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - ":"
                        - "Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiId
                        - "/*/POST/hello"

そしてPoliciesには具体的にどんなAWSのリソースにアクセスすることを許すのかの設定です。今回はLambdaをAPI GATEWAYを介して呼び出すアーキテクチャのための認証であると仮定して、API GATEWAYのコールを許可しています。

"Fn::ImportValue": ${self:custom.stage}-ApiGatewayRestApiIdの部分はすでにデプロイされているAPI Gatewayのリソースを取得していて、API Gatewayの当該パスにアクセスすることを許すという記載です。

デプロイの順番

IAMでAPI Gatewayを参照していると書きましたが、IAMはCognito Identity Poolに参照されています。つまり少なくともIAMとCognito Identity PoolのデプロイはAPI Gatewayの後でなければならないということです。Serverless Frameworkでマイクロサービスをデプロイするときはその順番も大事になります。

さいごに

Serverless Frameworkでデータベース、認証を構築する紹介をしました。次はAPI GATEWAYを紹介したいと思います。

maaaashin324
大手SIerでミッションクリティカルなシステムのシステムエンジニア、プロジェクトマネージャー、ITコンサルタントとカスタマーサポートに従事。 Code Chrysalis (https://www.codechrysalis.io/) のImmersiveコースを卒業してフルスタックソフトウェアエンジニアとなり、スタートアップのアプリケーション開発に携わったのち、株式会社yui 取締役CTO。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした