【第15回】Amazon Cognito + Spring Sercurityを使ったOAuth2 Loginの実装(4)


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


../_images/oauth2-login-flow.png


前回は、AWSコンソール上から、OAuth2 Loginに必要なアプリクライアントの設定を行い、IDプールを構築しました。 今回からは前回までにマネジメントコンソールで手動設定した内容をAWS CloudFormationを使って構築します。なお、CloudFormationを使って環境構築するメリットのひとつに、 アプリケーションからCloudFormationで構築したリソースの情報をスタック経由で参照できる点にあります。 こちらは連載 「AWSで実践!基盤構築・デプロイ自動化第34回」 でも詳しく解説しています。 今後OAuth2 Loginアプリケーションを作成する際には、CloudFormationのスタック情報を参照するように実装していきますので、その点、踏まえた上で、 今回からのCloudFormationによる環境構築の解説を読み進めていただければと思います。

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

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


前回までに作成した環境をCloudFormationを使って構築します。なお、このテンプレートは 第7回 などで構築した環境を前提として追加で実装したものです。そのため、事前に こちら の環境が作成されていることを前提に解説を進めます。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スペシャリスト ソフトウェアアーキテクト・デジタルテクノロジーストラテジスト(クラウド)

../_images/aws_361383_0752.jpeg

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

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

AWS Top Engineers & Ambassadors 選出。

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