こんにちは、あるいはこんばんは(アーニャ声)。 突然ですが認証認可周りの設計・実装は毎回全然違った要件に沿って違った感じでやっていく感じになるのでおもしろいですよね。 この記事では、先日Musubi Insightチームが社内向けに作成したコンソールの設計・実装について認証認可周りに絞ってまとめたいと思います。 認証認可はCognito User Groupベースで実装したのですが、意外とネットの情報が少なかったので参考になれば幸いです! なおブログ中に登場する画面のスクショについてなのですが、普段フロントエンドをほぼ触らない筆者がサクッと作ったものですので、デザインなどあれな部分もあるかと思います。ご容赦くださいませ(アーニャ声でごまかす)
背景
この社内コンソール(以下、Kizuki Console
とよびます。Kizukiはチームの名前です。)が作られた理由は、Musubi Insightの急激な成長です。
いくつか従前よりエンジニアが手動で対応していた作業があったのですが、それらにかかる負担がコミュニケーションコストなども含めるとかなり肥大化してきており、事業のスケーリングにおける障害になり始めていました。そのような状況を受けて、非エンジニアだけでもそういった作業を巻き取れるようにKizuki Consoleが爆誕する運びとなりました(Slack Ops的な選択肢も検討したのですがその辺りの話は本記事では割愛します)。
さて、認証・認可周りの具体的な要件は以下です:
- KAKEHASHI社内のGoogle IDを持っていない人はログイン不可
- KAKEHASHI社内のGoogle IDを持っている人は
- Kizuki memberグループ
に属していればKizuki memberグループ
の権限に応じて利用可能
- Kizuki supporterグループ
に属していればKizuki supporterグループ
の権限に応じて利用可能
- いずれのグループにも属していなければ利用不可
ユーザーグループを分けている理由としては、普段コンソールを使うメンバー(Kizuki member
)とSREなどの全社的な管理ユーザー的なメンバー(Kizuki supporter
)で将来権限に違いを出したくなるかもだから分けておこうという感じです。
以下の章で、具体的な設計と実装について触れていきます。
設計と実装
ざっくりした全体像
認証認可の設計の話に入る前に、周辺のスタックについて整理します。まずフロントエンドはSvelteで実装し、S3 + Cloudfrontの横綱構成でホスティングしました。フロントのCI/CDはAWS Amplifyでやっています。一方のバックエンドはAPIGateway + Lambdaの構成で、LambdaにはRDS proxyを挟んでAuroraにアクセスさせます。スタック全体はSAMで管理していて(Auroraは既存)CI/CDはGithub Actionsでやっています。上のポンチ絵のようなイメージです。
認証認可
さて、認証認可ですが、フロントエンドとバックエンドの両方にCognito User Pool(ID providerにGoogleを設定) + Cognito ID Poolによる認証認可の制限がかかっています。
バックエンド
Cognito User Pool(id providerはGoogle)に認証を任せてCognito ID Poolに認可を任せる、基本的な形が下敷きになっています(公式doc)。その基本形に、Cognito User GroupとIAM Roleを紐付けて各User Group特有の認可を与える要素が加わった感じになっています。頑張って再度ポンチ絵を作成したのでご査収ください。
これ以上リソース同士の関連を細かく書き下すと大変なことになるので、代わりにSAM templateのメイン部分を以下に示します。似たようなことをするときはこちらを元にすれば大体再現できるかと思います。(早口のアーニャ声でごまかす)
Google側の設定についてはこの記事などを参考にしてください。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Parameters: Stage: Default: dev Type: String AllowedValues: - local - dev - prod RdsSecretName: Type: String RdsSecretArn: Type: String SecurityGroups: Type: CommaDelimitedList Subnets: Type: CommaDelimitedList Globals: Function: Timeout: 60 MemorySize: 512 Runtime: python3.9 Tracing: Active CodeUri: functions/ Environment: Variables: rdsSecretName: !Ref RdsSecretName stage: !Ref Stage Resources: ListSuperusersFunction: Type: AWS::Serverless::Function Properties: Handler: list_superusers.lambda_handler VpcConfig: SecurityGroupIds: !Ref SecurityGroups SubnetIds: !Ref Subnets Policies: - AWSSecretsManagerGetSecretValuePolicy: SecretArn: !Ref RdsSecretArn Events: ListSuperusers: Type: Api Properties: Path: /list/superusers Method: get RestApiId: !Ref KizukiConsoleApi Auth: Authorizer: null # API Gateway KizukiConsoleApi: Type: AWS::Serverless::Api Properties: StageName: !Ref Stage Cors: AllowMethods: "'OPTIONS,POST,GET'" AllowHeaders: "'*'" AllowOrigin: "'*'" Auth: DefaultAuthorizer: AWS_IAM AddDefaultAuthorizerToCorsPreflight: false # IAM: Role for Kizuki members KizukiMemberRole: Type: AWS::IAM::Role Properties: 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 CognitoIdPool ForAnyValue:StringLike: cognito-identity.amazonaws.com:amr: - "authenticated" Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "execute-api:Invoke" Resource: - !Join [ "/", [!Join [ ":", [ "arn:aws:execute-api",!Ref 'AWS::Region',!Ref 'AWS::AccountId',!Ref KizukiConsoleApi]], "*"]] # IAM: Role for Kizuki supporters # 一旦メンバーと同じく全てのエンドポイントにアクセス可能にしておく KizukiSupporterRole: Type: AWS::IAM::Role Properties: 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 CognitoIdPool ForAnyValue:StringLike: cognito-identity.amazonaws.com:amr: - "authenticated" Policies: - PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "execute-api:Invoke" Resource: - !Join [ "/", [!Join [ ":", [ "arn:aws:execute-api",!Ref 'AWS::Region',!Ref 'AWS::AccountId',!Ref KizukiConsoleApi]], "*"]] # Cognito CognitoUserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: !Ref AWS::StackName UserPoolDomain: Type: AWS::Cognito::UserPoolDomain Properties: UserPoolId: !Ref CognitoUserPool Domain: !Sub "kizuki-console-${Stage}" CognitoClient: Type: AWS::Cognito::UserPoolClient Properties: UserPoolId: !Ref CognitoUserPool GenerateSecret: false PreventUserExistenceErrors: ENABLED CognitoIdPool: Type: AWS::Cognito::IdentityPool Properties: IdentityPoolName: !Sub "kizuki_console_id_pool_${Stage}" CognitoIdentityProviders: - ClientId: !Ref CognitoClient ProviderName: !GetAtt CognitoUserPool.ProviderName ServerSideTokenCheck: true AllowUnauthenticatedIdentities: false # User Group for Kizuki Members CognitoKizukiMemberGroup: Type: AWS::Cognito::UserPoolGroup Properties: Description: Kizuki members' group GroupName: kizuki-members RoleArn: !GetAtt KizukiMemberRole.Arn UserPoolId: !Ref CognitoUserPool # User Group for Kizuki Supporters CognitoKizukiSupporterGroup: Type: AWS::Cognito::UserPoolGroup Properties: Description: Kizuki supporters' group GroupName: kizuki-supporters RoleArn: !GetAtt KizukiSupporterRole.Arn UserPoolId: !Ref CognitoUserPool
フロントエンド
まずログインページ。ログインボタンを押すとaws-amplify
のAuth.federatedSignIn({provider: CognitoHostedUIIdentityProvider.Google,})}
が走ります。ログインが成功するとルートページに飛ぶようCognito UserPoolの設定がしてあります。コードとスクショのイメージは以下です。
<script> import { CognitoHostedUIIdentityProvider } from "@aws-amplify/auth"; import { Auth } from "aws-amplify"; import Button, { Label } from "@smui/button"; import Card, { Content, Actions, ActionButtons } from "@smui/card"; import Header from "../components/layout/Header.svelte"; </script> <Header /> <div class="login-card-container"> <Card> <Content style="margin:0 auto;"> <h3>ログイン</h3> </Content> <Actions> <ActionButtons style="display: flex; width: 100%; align-items: center; flex-direction: column;" > <Button on:click={() => Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google, })} variant="raised" > <Label>Googleでログインする</Label> </Button> </ActionButtons> </Actions> </Card> </div> <style> .login-card-container { margin: 80px auto 0 auto; width: 50%; } </style>
トップのSvelteコンポーネント(
App.svelte
)では都度Auth.currentAuthenticatedUser()
を呼び、認証情報を取得します。ユーザーがどのユーザーグループにも属していない場合以下のような画面に飛び、Cognito User Groupへの加入を促します。加入依頼を受けたらAWSのコンソールからCognitoの画面に行き、先に示したSAMテンプレートで作成済みのCognitoユーザーグループに依頼者を追加します。
その後依頼者が再ログインすると、ユーザーグループに紐づけてあるIAMの一時クレデンシャルをSTSが発行してくれるようになるので定義済みの認可ルールが適用されるようになります。(上のSAMテンプレートでは、作成したAPIの全エンドポイントを両方のユーザーグループのロールでinvokeできるようにしていますが、/member_only/
のようなエンドポイントを作ってPolicy documentの中でそれを指定することで叩けるエンドポイントを制限できたりします)
ログインが完了し、ユーザーグループへの追加も完了すると晴れて以下のようなメインページに遷移することができます。
終わりに
本記事では意外とネット上に情報の少ない(?)Cognito User Groupベースの認証認可について、フロントや運用周りも含めて頑張って解説を試みましたがいかがでしたでしょうか。 記事の詳細度の塩梅が幼児には難しかったです(アーニャ声でごまかす)。 どなたかの参考になれば嬉しいです。以上です!