Cognito でユーザープールベースのマルチテナンシーを選択した際に、API Gateway で複数ユーザープールを許可する Cognito ユーザープールオーソライザーを SAM で作成してみた

2023.03.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

いわさです。

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 トークンで通してみましたが、細かいアクセス制限をどのようにするべきかも今後考えていきたいと思います。