いわさです。
Cognito を認証サービスとして使用するアプリケーションでマルチテナントを実現しようとする場合、次のようにいくつかの選択肢があります。
マルチテナントの認証・認可要件によってどれを選択するべきか変わってくるのですが、ユーザープール単位で共通設定されるオプションがテナントごとに差異が出る場合はユーザープールベースのマルチテナンシーを選択する形になります。
例えば、テナント A と テナント B で MFA の有無やパスワードポリシーが異なる場合などです。
ただし上記ドキュメントのユーザープールベースのシナリオのうち、以下について気になりました。
アプリケーションに、各テナントがその使用のためにアプリケーションインフラストラクチャの完全なインスタンスを取得する、サイロ型のマルチテナントアプリケーションがある。
ユーザープールごとにサイロ型で分離されたインフラ環境であればマルチテナンシーを実現出来るのはまぁそうかという感じです。
ただし、今回は API Gateway をベースにアーキテクチャーを検討していました。
その場合でもテナントごとに用意する必要があるのでしょうか。そういえば、ひとつの統合で設定出来るオーソライザーは 1 つまでです。
カスタムオーソライザーであれば根性入れればどうにかなりそうですが、Cognito ユーザープールオーソライザーについてはちょっと実現性について自信がないです。
そこで、今回はユーザープールベースのマルチテナンシー戦略を採用した場合に API Gateway の Cognito ユーザープールオーソライザーを使えるのかどうかを検証してみたいと思います。
API Gateway は REST API です。
適当なサーバーレスアプリケーション を作成
まずは検証対象の適当なサーバーレスアプリを作成します。
sam init
して適当なテンプレートの関数をいくつか複製しておきます。
次に Application Composer を使って Lambda に色々とコンポーネントを接続していきます。
最後にテンプレートをまた少し手動で調整します。
以下のような感じにしました。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: '---'
Globals:
Function:
Timeout: 10
MemorySize: 256
Architectures:
- x86_64
Runtime: dotnet6
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src/HelloWorld/
Handler: HelloWorld::HelloWorld.Function::FunctionHandler
Events:
HogeApiGET:
Type: Api
Properties:
Path: /
Method: GET
RestApiId: !Ref HogeApi
HogeFunction1:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src/HogeFunc1/
Handler: HogeFunc1::HogeFunc1.Function::FunctionHandler
Events:
HogeApiGEThoge1:
Type: Api
Properties:
Path: /hoge1
Method: GET
RestApiId: !Ref HogeApi
Auth:
Authorizer: Auth1
HogeFunction2:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src/HogeFunc2/
Handler: HogeFunc2::HogeFunc2.Function::FunctionHandler
Events:
HogeApiGEThoge2:
Type: Api
Properties:
Path: /hoge2
Method: GET
RestApiId: !Ref HogeApi
Auth:
Authorizer: Auth2
HogeApi:
Type: AWS::Serverless::Api
Properties:
Name: hoge0313Api
StageName: Prod
EndpointConfiguration: REGIONAL
TracingEnabled: true
Auth:
Authorizers:
Auth1:
UserPoolArn: !GetAtt UserPool1.Arn
Auth2:
UserPoolArn: !GetAtt UserPool2.Arn
UserPool1:
Type: AWS::Cognito::UserPool
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
AliasAttributes:
- email
- preferred_username
UserPoolName: !Sub ${AWS::StackName}-UserPool1
UserPool2:
Type: AWS::Cognito::UserPool
Properties:
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AliasAttributes:
- email
- preferred_username
UserPoolName: !Sub ${AWS::StackName}-UserPool2
UserPoolClient1:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool1
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
UserPoolClient2:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool2
ExplicitAuthFlows:
- ALLOW_USER_PASSWORD_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
こちらをデプロイすると、ルート含めて 3 つのリソースが用意されている API が作成されます。
ルートはオーソライザーは設定されておらず、hoge1
にはオーソライザー A が、hoge2
にはオーソライザー B が設定されています。
匿名アクセスしてみると次のようになります。
% curl https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod
{"message":"hello world","location":"3.115.15.210"}
% curl https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge1
{"message":"Unauthorized"}
% curl https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge2
{"message":"Unauthorized"}
hoge1 と hoge2 は Cognito ユーザープールのトークンが必要なので、期待どおりの動作ですね。
Application Composer はスケルトンコードの生成は弱いのと、細かいところは最終的にはテンプレート修正で調整する必要があるので、今回のような使い方を私は最近しています。
1 つのオーソライザーに複数の Cognito ユーザープールを追加
今の状態だと、hoge1 はユーザープール 1 でアクセス可能、hoge2 はユーザープール 2 でアクセス可能という形になっています。
これはプール型では非常に良くないです。テナントの数だけリソースが増えてしまう。
そこで hoge1 にはどちらにテナントでもアクセス出来るようにしてみたいと思います。
マネジメントコンソールからは 1 つのオーソライザーに指定出来るユーザープールは 1 つなのですが、試してみたところどうやら API や IaC で指定する場合はオーソライザーのユーザープール ARN は複数の指定することも出来るようです。
次のように単独のオーソライザーに複数テナントを想定したユーザープールを指定してみました。
:
HogeApi:
Type: AWS::Serverless::Api
Properties:
Name: hoge0313Api
StageName: Prod
EndpointConfiguration: REGIONAL
TracingEnabled: true
Auth:
Authorizers:
Auth1:
UserPoolArn:
- !GetAtt UserPool1.Arn
- !GetAtt UserPool2.Arn
Auth2:
UserPoolArn: !GetAtt UserPool2.Arn
UserPool1:
Type: AWS::Cognito::UserPool
:
デプロイ後にマネジメントコンソールを確認してみると...
複数設定されていますね。これはいけそうだ。
あとはトークンを取得してそれぞれの API へアクセスしてみましょう。
今回は以下の記事を参考に AWS CLI 経由でトークンを取得してみました。
% cat hogeuser1.json
{
"AuthFlow": "USER_PASSWORD_AUTH",
"ClientId": "4ekqh42ac992nj5a08iaqtqjqn",
"AuthParameters": {
"USERNAME": "hogeuser1",
"PASSWORD": "hogehoge"
}
}
% aws cognito-idp initiate-auth --cli-input-json file://hogeuser1.json
{
"ChallengeParameters": {},
"AuthenticationResult": {
"AccessToken": "eyJraWQiO...lKNpu0_Q",
"ExpiresIn": 3600,
"TokenType": "Bearer",
"RefreshToken": "eyJjdHkiOiJ...4hLYlhiUKbAg",
"IdToken": "eyJraWQiOi...wUf5b_dAQ"
}
}
テナント 1 のユーザー
% curl -H "Authorization: eyJr..._dAQ" "https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge1"
{"message":"HogeFunc1"}
% curl -H "Authorization: eyJr..._dAQ" "https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge2"
{"message":"Unauthorized"}
テナント 2 のユーザー
% curl -H "Authorization: eyJr...SJdg" "https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge1"
{"message":"HogeFunc1"}
% curl -H "Authorization: eyJr...SJdg" "https://vj3la9qlde.execute-api.ap-northeast-1.amazonaws.com/Prod/hoge2"
{"message":"HogeFunc2"}
複数のユーザープールを紐付けたオーソライザーでは対象のユーザープールからのアクセスを許可することが出来ました。
オーソライザーに関連付けるユーザープールの上限
API あたりのオーソライザーの最大は 10 (上限緩和可能) という情報が公開されています。
ただし、1 オーソライザーに関連付け出来るユーザープールの上限は不明です。
参考までに、私が試したところ 51 個までは関連付け出来ることを確認しました。
さいごに
本日は Cognito でユーザープールベースのマルチテナンシーを選択した際に、オーソライザーに複数のユーザープールを紐付けて API Gateway で使ってみました。
結論としては単一の Cognito ユーザープールオーソライザーに複数のユーザープールを設定することが出来ました。
今回はシンプルに ID トークンで通してみましたが、細かいアクセス制限をどのようにするべきかも今後考えていきたいと思います。