前回から、以下のイメージのようにOAuth2 Loginをベースとしたアーキテクチャを想定した環境構築を進めています。

構成図

前回は、AWSコンソール上からOAuth2 Loginに必要なアプリクライアントの設定を行い、IDプールを構築しました。今回からは、前回までにマネジメントコンソールで手動設定した内容をAWS CloudFormationを使って構築します。

なお、CloudFormationを使って環境構築するメリットの1つに、アプリケーションからCloudFormationで構築したリソースの情報をスタック経由で参照できる点にあります。今後、OAuth2 Loginアプリケーションを作成する際には、CloudFormationのスタック情報を参照するように実装していくので、その点を踏まえた上で、今回からの解説を読み進めていただければと思います。

なお、実際のソースコードはGitHub上にコミットしています。以降のソースコードでは、本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

※ こちらは連載「AWSで実践!基盤構築・デプロイ自動化」の第34回でも詳しく解説しています。

CloudFormationを使ったCognitoユーザープール、IDプールの構築

以降の解説は、第7回で構築した環境を前提として進めます。では早速、前回までに作成した環境をCloudFormationを使って構築していきましょう。CloudFormationテンプレートは以下の通りです。

AWSTemplateFormatVersion: '2010-09-09'

# omit

Parameters:
  # omit
  EnvType:                                                               #(A)
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Dev", "Staging", "Production"]
    Default: Dev

 Conditions:                                                             #(B)
   ProductionResources: {"Fn::Equals" : [{"Ref":"EnvType"}, "Production"]}
   StagingResources: !Equals [ !Ref EnvType, "Staging"]
   DevResources: {"Fn::Equals" : [{"Ref":"EnvType"}, "Dev"]}

 Resources:
   MynaviSampleUserPool:                                                 #(C)
     Type: AWS::Cognito::UserPool
     Properties:
       UserPoolName: !If ["ProductionResources", "mynavi-sample-microservice-userpool",  !If ["StagingResources", "staging_mynavi-sample-microservice-userpool", "dev_mynavi-sample-microservice-userpool"]]
                                                                         #(D)
       AliasAttributes:
         - email
       UsernameConfiguration:
         CaseSensitive: false
       Policies:
         PasswordPolicy:
           MinimumLength: 6
           RequireLowercase: true
           RequireNumbers: false
           RequireSymbols: false
           RequireUppercase: false
       Schema:
         - Name: family_name
           AttributeDataType: String
           Mutable: true
           Required: true
         - Name: given_name
           AttributeDataType: String
           Mutable: true
           Required: true
         - Name: loginId
           AttributeDataType: String
           Mutable: false
           Required: false
         - Name: isAdmin
           AttributeDataType: Number
           Mutable: true
           Required: false
           NumberAttributeConstraints:
             MinValue: "0"
             MaxValue: "2"

  # omit

  MynaviBackendAppClient:                                                #(E)
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: !If ["ProductionResources", "mynavi-sample-microservice-backend-app",  !If ["StagingResources", "staging_mynavi-sample-microservice-backend-app", "dev_mynavi-sample-microservice-backend-app"]]
      GenerateSecret: true
      RefreshTokenValidity: 30
      UserPoolId : !Ref MynaviSampleUserPool
      CallbackURLs:
        - !If ["ProductionResources", "https://xxxx/login/oauth2/code/cognito",  !If ["StagingResources", "https://xxxx/login/oauth2/code/cofnito", "http://localhost:8080/frontend/login/oauth2/code/cognito"]]
      LogoutURLs:
        - !If ["ProductionResources", "https://xxxx/",  !If ["StagingResources", "https://xxxx/", "http://localhost:8080/frontend"]]
      AllowedOAuthFlows:
        - code
      AllowedOAuthScopes:
        - openid
        - aws.cognito.signin.user.admin
        - profile
      AllowedOAuthFlowsUserPoolClient: true
      SupportedIdentityProviders:
        - COGNITO
      ExplicitAuthFlows:
        - ALLOW_ADMIN_USER_PASSWORD_AUTH
        - ALLOW_REFRESH_TOKEN_AUTH

  MynaviSampleUserPoolDomain:                                            #(F)
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: debugroom-mynavi-sample-microservice
      UserPoolId: !Ref MynaviSampleUserPool

  MynaviSampleIdentityPool:                                              #(G)
    Type: AWS::Cognito::IdentityPool
    Properties:
      IdentityPoolName:  !If ["ProductionResources", "mynavi-sample-microservice-idpool",  !If ["StagingResources", "staging_mynavi-sample-microservice-idpool", "dev_mynavi-sample-microservice-idpool"]]
      AllowUnauthenticatedIdentities: false
      CognitoIdentityProviders:
        - ClientId:
            Ref: MynaviBackendAppClient
          ProviderName:
            Fn::Join:                                                    #(H)
              - ""
              - - "cognito-idp."
                - !Sub ${AWS::Region}
                - ".amazonaws.com/"
                - !Ref MynaviSampleUserPool

  MynaviSampleUnauthenticatedPolicy:                                     #(I)
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - mobileanalytics:PutEvents
              - cognito-sync:*
            Resource:
              - "*"

  MynaviSampleUnauthenticatedRole:                                       #(J)
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action: "sts:AssumeRoleWithWebIdentity"
            Principal:
              Federated: cognito-identity.amazonaws.com
            Condition:
              StringEquals:
                "cognito-identity.amazonaws.com:aud": !Ref MynaviSampleIdentityPool
              ForAnyValue:StringLike:
                "cognito-identity.amazonaws.com:amr": unauthenticated
      ManagedPolicyArns:
        - !Ref MynaviSampleUnauthenticatedPolicy

  MynaviSampleAuthenticatedPolicy:                                       #(K)
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - mobileanalytics:PutEvents
              - cognito-sync:*
              - cognito-identity:*
            Resource:
              - "*"

  MynaviSampleAuthenticatedRole:                                         #(L)
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action: "sts:AssumeRoleWithWebIdentity"
      Principal:
        Federated: cognito-identity.amazonaws.com
      Condition:
        StringEquals:
          "cognito-identity.amazonaws.com:aud": !Ref MynaviSampleIdentityPool
        ForAnyValue:StringLike:
          "cognito-identity.amazonaws.com:amr": authenticated
    ManagedPolicyArns:
      - !Ref MynaviSampleAuthenticatedPolicy

  RoleAttachment:                                                        #(M)
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId: !Ref MynaviSampleIdentityPool
      Roles:
        unauthenticated : !GetAtt MynaviSampleUnauthenticatedRole.Arn
        authenticated: !GetAtt MynaviSampleAuthenticatedRole.Arn

Outputs:
  MynaviSampleUserPool:                                                  #(O)
    Description: UserPool ID
    Value: !Ref MynaviSampleUserPool
    Export:
      Name: !Sub ${VPCName}-Cognito-${EnvType}-UserPool

 # omit

  MynaviSampleBackendAppClientID:                                        #(P)
    Description: BackendApp Client
    Value: !Ref MynaviBackendAppClient
    Export:
      Name: !Sub ${VPCName}-Cognito-${EnvType}-AppClientId

  MynaviSampleRedirectUri:                                               #(Q)
    Description: RedirectUri
    Value: !If ["ProductionResources", "https://xxxx/login/oauth2/code/cognito",  !If ["StagingResources", "https://xxxx/login/oauth2/code/cofnito", "http://localhost:8080/frontend/login/oauth2/code/cognito"]]
    Export:
      Name: !Sub ${VPCName}-Cognito-${EnvType}-RedirectUri

  MynaviSampleJwkSetUri:                                                 #(R)
    Description: jwk-set-uri
    Value:
      Fn::Join:
        - ""
        - - "https://cognito-idp."
          - !Sub ${AWS::Region}
          - ".amazonaws.com/"
          - !Ref MynaviSampleUserPool
          - "/.well-known/jwks.json"
    Export:
      Name: !Sub ${VPCName}-Cognito-${EnvType}-JwkSetUri

  MynaviSampleUserPoolDomain:                                            #(S)
    Description: User Pool Domain
    Value:
      Fn::Join:
        - ""
        - - "https://"
          - !Ref MynaviSampleUserPoolDomain
          - ".auth."
          - !Sub ${AWS::Region}
          - ".amazoncognito.com"
    Export:
      Name: !Sub ${VPCName}-Cognito-${EnvType}-UserPoolDomain
コードの各ポイントでは、以下に従ってアプリクライアントの設定を行います。
項番 説明
A 環境を「EnvType」パラメータとして指定します。パラメータの値に応じて、名称やドメインURLなど切り替えるために使用します
B Aで指定したパラメータを用いてConditions要素で切り替えます。Conditionsの使用方法については、連載「AWSで実践!基盤構築・デプロイ自動化」の第29回も参考にしてください
C ユーザープールを定義します。各プロパティの詳細は「AWS::Cognito::UserPool」も参考にしてください。設定内容は第13回と同様になるようにしています
D Bで定義したConditionsを使ってユーザープール名を環境ごとに切り替えて作成するようにします
E アプリクライアントを定義します。各プロパティの詳細は「AWS::Cognito::UserPoolClient」も参考にしてください。今回、設定内容は第14回と同様になるようにしています
F ユーザープールのドメインを定義します。各プロパティの詳細は「AWS::Cognito::UserPoolDomain」も参考にしてください。今回、設定内容は第14回と同様になるようにしています
G IDプールのドメインを定義します。各プロパティの詳細は「AWS::Cognito::IdentityPool」も参考にしてください。今回、設定内容は第14回と同様になるようにしています
H JOIN関数を使用してプロバイダ名を生成します
I IDプールに設定する非認証ユーザーのポリシーを定義します。各プロパティの詳細は、「AWS::IAM::ManagedPolicy」も参考にしてください。今回、設定内容は第14回で作成されるものと同様になるようにしています
J IDプールに設定する非認証ユーザーのロールを定義します。各プロパティの詳細は、「AWS::IAM::Role」も参考にしてください。今回、設定内容は第14回で作成されるものと同様になるようにしています
K IDプールに設定する認証ユーザーのポリシーを定義します。各プロパティの詳細は、「AWS::IAM::ManagedPolicy」も参考にしてください。今回、設定内容は第14回で作成されるものと同様になるようにしています
L IDプールに設定する認証ユーザーのロールを定義します。各プロパティの詳細は、「AWS::IAM::Role」も参考にしてください。今回、設定内容は第14回で作成されるものと同様になるようにしています
M IDプールに設定するロールアタッチメントを定義します。各プロパティの詳細は、「AWS::Cognito::IdentityPoolRoleAttachment」も参考にしてください。J、Lで作成したロールをアタッチします
O 今後解説するLambdaファンクションやOAuth2 Loginアプリケーション実装でスタック情報経由で参照するため、ユーザープールIDを出力します
P 今後解説するLambdaファンクションやOAuth2 Loginアプリケーション実装でスタック情報経由で参照するため、アプリクライアントIDを出力します
Q 今後解説するOAuth2 Loginアプリケーション実装でスタック情報経由で参照するため、OAuth2ログイン後のリダイレクトURLを出力します
R 今後解説するOAuth2 Loginアプリケーション実装でスタック情報経由で参照するため、CognitoユーザープールのJWK:JSON Web Key(公開鍵)のURLを出力します
S 今後解説するOAuth2 Loginアプリケーション実装でスタック情報経由で参照するため、ユーザープールのドメインを出力します

CloudFormationのOutputでは、実装アプリケーションに必要なアプリクライアントのクライアントシークレット値を出力することはできません。そのため、アプリケーションの実装では、Systems Manager Parameter Store経由でクライアントシークレットを取得する実装にするものとします。

SDKを使って構築したアプリクライアントからクライアントシークレットを取得し、Parameter StoreにセットするLambdaファンクションを作成した上で、CloudFormationのカスタムリソースとして実行する方法については、次回紹介します。

* * *

今回は、OAuth2 Loginを行う場合の、CognitoのCloudFormationテンプレートについて解説しました。次回以降は、OAuth2 Loginアプリケーションを実装する前に、構築したCognitoの初期化として、ユーザープールへユーザーを追加する方法や、構築したアプリクライアントからParameter Storeへクライアントシークレットを設定するLambda関数を実装し、CloudFormationのカスタムリソースとして実行する方法について解説していきます。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ
エグゼクティブ ITスペシャリスト ソフトウェアアーキテクト・デジタルテクノロジーストラテジスト(クラウド)

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。