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

2023.04.24

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

いわさです。

Amazon Cognito では認証要件に応じていくつかのマルチテナント分離戦略を取ることが出来ます。
最も分離性が高いものはテナントごとにユーザープールを分離するパターンです。ユーザープールごとに共通設定となるもの(パスワードポリシーとか)をテナントごとに違うもの設定する場合などはこの戦略が取られることがあります。

そうした時に Cognito から取得されるトークンをどのように検証したら良いでしょうか。
Cognito オーソライザーを使う場合は、オーソライザーに対象のユーザープールを複数関連付けすることが出来ます。

これは以前次の記事で紹介させて頂きました。

しかし、Cognito ユーザープールではなく Lambda オーソライザーを使うシーンは多いと思います。
本日は Lambda オーソライザーでの検証方法を紹介し、aws-jwt-verify で実際に検証を行ってみました。

Lambda オーソライザーではクレームを検証する

Lambda オーソライザーでどのようにユーザープールの判断をすれば良いのでしょうか。
トークン検証方法は次のドキュメントで案内されています。

上記に記述されているように、JWT クレームからトークンの発行者などを確認すれば良いです。

  • アプリクライアント ID はaud(ID トークンの場合)もしくはclient_id(アクセストークンの場合)
  • ユーザープールはiss
  • ID トークンかアクセストークンかはtoken_use

aws-jwt-verify で検証

また、Node.js の場合だと AWS JWT Verify ライブラリ(aws-jwt-verify)を使って Cognito が発行するトークンを簡単に検証することが出来るのですが、今回このライブラリが複数ユーザープールをサポートしていることを知りました。
今回はこの機能を試してみます。

次のように 3 つのユーザープール(テナントを想定)を作成し、テナント 1 とテナント 2 のユーザーのみが API Gateway の対象リソースへアクセス出来るようにしてみたいと思います。
今回は JWT が有効で指定したユーザープールのものであれば API へのアクセスを許可する簡易的なものにしてみようと思います。

API Gateway 側はレスポンス出来れば何でも良いので、おなじみの Mock 統合で適当な静的レスポンスを返すようにします。

この時点ではアクセス出来ますね。

% curl https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge
{
    "hoge": "hogevalue111"
}

次のような Lambda 関数を作成します。
ライブラリの使い方の詳細はリファレンスをご確認頂ければと思いますが、以下のハイライト部分で複数のユーザープール情報を指定しています。
ここでは ID トークンで、アプリケーションクライアント ID も指定しています。(チェック対象外にすることも可能。後述します)

index.mjs

import { CognitoJwtVerifier } from "aws-jwt-verify";

export const handler = async(event) => {
    const idTokenVerifier = CognitoJwtVerifier.create([
        {
            userPoolId: "ap-northeast-1_Mdi0XA8go",
            tokenUse: "id",
            clientId: "7e68gbh25j0095au92v0rhmtdd",
        },
        {
            userPoolId: "ap-northeast-1_7HgNuTmeX",
            tokenUse: "id",
            clientId: "4qut9445vbc8u9dhthbtrghc22",
        },
    ]);
    
    var resourceEffect = 'Deny';
    try {
        var token = event.authorizationToken
        const idTokenPayload = await idTokenVerifier.verify(token);
        console.log("Token is valid. Payload:", idTokenPayload);
        resourceEffect = 'Allow';
    } catch {
        console.log("Token not valid!");
    }

    var documentRoot = {};
    documentRoot.Version = '2012-10-17'; 
    documentRoot.Statement = [];
    
    var statementOne = {};
    statementOne.Action = 'execute-api:Invoke'; 
    statementOne.Effect = resourceEffect;
    statementOne.Resource = event.methodArn;
    documentRoot.Statement[0] = statementOne;
    return {
        principalId: 'abc123',
        policyDocument: documentRoot,
    };
};

上記関数を指定した Lambda オーソライザーを作成し、API Gateway の対象リソースの認可に使います。

トークンなしでアクセスしたところ次のように拒否されました。
良いですね。

% curl -H "Authorization: aaaa" https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge/
{"Message":"User is not authorized to access this resource with an explicit deny"}

トークンを使う

テナント 1 のユーザーでinitiate-authを使って Cognito ユーザープールで認証します。
ID トークンやアクセストークンなどを取得することが出来ます。

% aws cognito-idp initiate-auth --cli-input-json file://initiate-auth.json
{
    "ChallengeParameters": {},
    "AuthenticationResult": {
        "AccessToken": "hogehoge-access-token",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": "....",
        "IdToken": "hogehoge-id-token"
    }
}

次に、取得した ID トークンを使って API へリクエストしてみます。

% curl -H "Authorization: hogehoge-id-token" https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge/
{
    "hoge": "hogevalue111"
}

アクセス出来ましたね。
ちなみにアクセストークンだと拒否されました。

% curl -H "Authorization: hogehoge-access-token" https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge/
{"Message":"User is not authorized to access this resource with an explicit deny"}

テナント 2 の場合も上記と同様の挙動でした。
しかし、テナント 3 は拒否されるはずです。どうかな。

% curl -H "Authorization: tenant3-id-token" https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge/
{"Message":"User is not authorized to access this resource with an explicit deny"}

拒否されましたね。

アクセストークンも通すようにカスタマイズ

先程少し触れましたが、コード上でどのユーザープールのどのトークン種類で、アプリケーションクライアントは何かまで指定しています。
tokenUseclientIdについてはnullを設定することでチェックをスキップすることが出来ます。(対象ユーザープールであれば全部通したい場合に使える)

index.mjs

import { CognitoJwtVerifier } from "aws-jwt-verify";

export const handler = async(event) => {
    const idTokenVerifier = CognitoJwtVerifier.create([
        {
            userPoolId: "ap-northeast-1_Mdi0XA8go",
            tokenUse: null,
            clientId: "7e68gbh25j0095au92v0rhmtdd",
        },
        {
            userPoolId: "ap-northeast-1_7HgNuTmeX",
            tokenUse: "id",
            clientId: "4qut9445vbc8u9dhthbtrghc22",
        },
    ]);
    
    var resourceEffect = 'Deny';
:

テナント 1 ユーザーのアクセストークンを付与してリクエストしてみます。

% curl -H "Authorization: hogehoge-access-token" https://94lj57p0gd.execute-api.ap-northeast-1.amazonaws.com/hoge/
{
    "hoge": "hogevalue111"
}

今度はアクセストークンでも API を使用出来ました。

さいごに

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

aws-jwt-verify を使う場合でも複数ユーザープールを対象とする Lambda オーソライザーが作成出来ましたね。
あとはユーザープール情報をコード上に埋め込んでいる状態なので、パラメータストアあたりから取得しれやればトークン検証部分は良さそうな感じがしますね。