前回から、以下のイメージのように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 まで。