[アップデート] Amazon Verified Permission の新しいセットアップオプションで、API Gateway + Cognito を対象とした認可ロジックやポリシーストアを簡単に作成出来るようになりました

2024.04.07

いわさです。

Amazon Verified Permissions はみなさん使ってますかね。たぶんまだそう多く使われてないですよね。あるいは全く知らないという方も多いと思います。

私は API Gateway + Lambda + Cognito のサーバーレス構成が結構好きなのですが、認可周りに独自実装が必要になるのがちょっとイマイチだなぁと思っていました。
そんな中 Amazon Verified Permissions が登場し、この認可部分をちょっと AWS 管理に寄せることが出来るようになったのですが、それでも独自の認可コードの実装は必要でした。

しかし、先日、Amazon Verified Permissions に新しいアップデートがあり、この認可周りの設定を非常に簡単に設定出来るようになりました。

先にまとめ

このアップデート、これまで独自実装として準備が必要だったコンポーネントやコードを Verified Permissions が CloudFormation スタックとして自動作成してくれるようになります。

具体的には Verified Permissions がセットアップの中で次のようなことをしてくれます。

  • API Gateway で使用する Lambda オーソライザーを自動で作成(オーソライザーも Lambda コードも)
  • Verified Permission の画面で、選択した API Gateway のリソースに基づく認可設定を Cognito グループ単位でポチポチ設定することで、自動で Verified Permission のポリシーを生成してくれる

API Gateway + Cognito を使っている場合に限りますが、このアップデートでかなり直感的に、楽に認可設定が出来るようになります。
Verified Permissoin の画面でポチポチして、最後に作成されたカスタムオーソライザーを API Gateway に関連付けるだけです。超簡単です。

ただ、ポリシーストア作成時の初回のセットアップ時に使える機能で、既存のポリシーストアに API Gateway のリソース情報を追加反映することは出来無さそうでした。

やってみましょう

使い勝手を知りたいので実際に試してみました。
先にまとめた内容が全てですけども、検証した結果も紹介しますね。

適当な API Gateway + Cognito を用意しておく

まずは API Gateway と Cognito プールを用意します。
後述しますが、Verified Access でセットアップを行う時に、ステージへデプロイ済みの API Gateway と Cognito ユーザープールの選択が必須なので先に用意しましょう。

API Gateway でモック統合した適当な REST API を作成します。

許可と拒否のリソースを混在させたかったのでとりあえず2つ作成しました。

% curl https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/fuga
fuga!!!

% curl https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/hoge
hoge!!!

Cognito ユーザープールを新規作成し、グループを作成します。
今回の機能はグループ単位で認可設定を行える仕様となっています。

で、適当なユーザーも作成し、グループへ所属させておきましょう。
今回はグループ1にユーザーAを。グループ2にユーザーBを所属させてみました。

% aws cognito-idp list-users-in-group --user-pool-id ap-northeast-1_SNXO2a4n0 --group-name hoge0407group1 --profile hoge
{
    "Users": [
        {
            "Username": "27147a38-00c1-709a-1871-53d8cb5cfdef",
            "Attributes": [
                {
                    "Name": "email",
                    "Value": "hoge0407a@example.com"
                },
                {
                    "Name": "email_verified",
                    "Value": "true"
                },
                {
                    "Name": "sub",
                    "Value": "27147a38-00c1-709a-1871-53d8cb5cfdef"
                }
            ],
            "UserCreateDate": "2024-04-07T17:54:31.481000+09:00",
            "UserLastModifiedDate": "2024-04-07T18:15:13.088000+09:00",
            "Enabled": true,
            "UserStatus": "CONFIRMED"
        }
    ]
}
% aws cognito-idp list-users-in-group --user-pool-id ap-northeast-1_SNXO2a4n0 --group-name hoge0407group2 --profile hoge
{
    "Users": [
        {
            "Username": "87341a48-7021-7054-dce0-1552de0fafdc",
            "Attributes": [
                {
                    "Name": "email",
                    "Value": "hoge0407b@example.com"
                },
                {
                    "Name": "email_verified",
                    "Value": "true"
                },
                {
                    "Name": "sub",
                    "Value": "87341a48-7021-7054-dce0-1552de0fafdc"
                }
            ],
            "UserCreateDate": "2024-04-07T17:54:46.450000+09:00",
            "UserLastModifiedDate": "2024-04-07T18:17:05.266000+09:00",
            "Enabled": true,
            "UserStatus": "CONFIRMED"
        }
    ]
}

Verified Permissions で API Gateway + Cognito セットアップ機能を使う

ここからが今回のアップデート部分になります。
Amazon Verified Permissions コンソールへアクセスし、新しいポリシーストア作成を開始します。

ポリシーストア作成ウィザードの起動オプションでで「コグニートと API ゲートウェイによるセットアップ - 新品です」を選択します。新品です!?

続いて対象の API Gateway を選択します。
まずは先ほど作成した REST API とステージを選択しましょう。

ステージまで選択したら、「API をインポート」を押します。
そうすると、API Gateway のリソースとメソッドがリストアップされ、自動でシーダーアクションとしてマッピングされます。

続いて Cognito ユーザープールを選択します。
Verified Permission 自体は Cognito ユーザープール以外でも使えますし、API Gateway のカスタムオーソライザーも Cognito ユーザープール以外で使えますが、本機能については Cognito ユーザープールのみがサポートされています。

対象のユーザープール選択と併せて、対象トークンタイプとクライアント検証を行うかを選択します。
トークンタイプは Lambda オーソライザーの環境変数に、クライアント検証オプションはポリシーストアのクライアント検証に影響します。

続いて、認可設定を行う Cognito ユーザープールのグループを選択し、先にインポートした API Gateway からマッピングされた各アクションに対して許可するか拒否するかを設定します。
ここで設定した内容が Verified Permissions のポリシーとして設定される形になります。

あとはデプロイを実行します。
ポリシーストアなどは即作成されますが、カスタムオーソライザーなどは CloudFormation スタックとして作成されます。
この画面で待機する必要はないので、しばらく経ったら Verified Permission コンソールにアクセスして再確認すれば良いです。

作成されたリソースを確認する

今回のセットアップ機能でどういうリソースがデプロイされたのかを確認してみます。

まずは当然ながら Verified Permissions のポリシーストアが作成されています。
セットアップ中に選択した API Gateway パスや Cognito ユーザープールのグループであるかを検証する内容です。

API Gateway を確認してみると Lambda オーソライザーが作成されています。
ID ソースが Authorization ヘッダー、HTTP メソッド、リソースパスとなっています。
また、認可キャッシュが 120 秒になっていますね。
なるほど、Verified Permissions 使いすぎるとまぁまぁ高額になるんじゃないかと思っていたのですが、キャッシュを使うのが良いかもしれない。

ただしこのオーソライザー、API Gateway 側でに設定はされていません。
必要に応じて設定し、ステージへデプロイする必要があります。これは後で試しますね。

カスタムオーソライザーで使われる Lambda 関数は次のようなものになっています。
汎用的に使えるというか、Verified Permissions の検証メソッドは Cognito ユーザープールでしか使えないものだったと思いますが、ポリシーストアで認可ロジックを吸収出来る感じになっていますね。

対象ポリシーストア ID など、セットアップ時の内容にで動的になる部分は環境変数で設定されていました。

ちなみにデプロイされていた関数コードは次のようなものです。ランタイムはNode.js 20.xでした。

index.js

const { VerifiedPermissions } = require('@aws-sdk/client-verifiedpermissions');
const policyStoreId = process.env.POLICY_STORE_ID;
const namespace = process.env.NAMESPACE;
const tokenType = process.env.TOKEN_TYPE;
const resourceType = `${namespace}::Application`;
const resourceId = namespace;
const actionType = `${namespace}::Action`;

const verifiedpermissions = !!process.env.ENDPOINT
  ? new VerifiedPermissions({
    endpoint: `https://${process.env.ENDPOINT}ford.us-west-2.amazonaws.com`,
  })
  : new VerifiedPermissions();

function getContextMap(event) {
  const hasPathParameters = Object.keys(event.pathParameters).length > 0;
  const hasQueryString = Object.keys(event.queryStringParameters).length > 0;
  if (!hasPathParameters && !hasQueryString) {
    return undefined;
  }
  const pathParametersObj = !hasPathParameters ? {} : {
    pathParameters: {
      // transform regular map into smithy format
      record: Object.keys(event.pathParameters).reduce((acc, pathParamKey) => {
        return {
          ...acc,
          [pathParamKey]: {
            string: event.pathParameters[pathParamKey]
          }
        }
      }, {}),
    }
  };
  const queryStringObj = !hasQueryString ? {} : {
    queryStringParameters: {
      // transform regular map into smithy format
      record: Object.keys(event.queryStringParameters).reduce((acc, queryParamKey) => {
        return {
          ...acc,
          [queryParamKey]: {
            string: event.queryStringParameters[queryParamKey]
          }
        }
      }, {}),
    }
  };
  return {
    contextMap: {
      ...queryStringObj,
      ...pathParametersObj,
    }
  };
}

async function handler(event, context) {
  // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html
  // > Header names and query parameters are processed in a case-sensitive way.
  // https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2
  // > header field names MUST be converted to lowercase prior to their encoding in HTTP/2
  // curl defaults to HTTP/2
  let bearerToken =
    event.headers?.Authorization || event.headers?.authorization;
  if (bearerToken?.toLowerCase().startsWith('bearer ')) {
    // per https://www.rfc-editor.org/rfc/rfc6750#section-2.1 "Authorization" header should contain:
    //  "Bearer" 1*SP b64token
    // however, match behavior of COGNITO_USER_POOLS authorizer allowing "Bearer" to be optional
    bearerToken = bearerToken.split(' ')[1];
  }
  try {
    const parsedToken = JSON.parse(Buffer.from(bearerToken.split('.')[1], 'base64').toString());
    const actionId = `${event.requestContext.httpMethod.toLowerCase()} ${event.requestContext.resourcePath}`;

    const input = {
      [tokenType]: bearerToken,
      policyStoreId: policyStoreId,
      action: {
        actionType: actionType,
        actionId: actionId,
      },
      resource: {
        entityType: resourceType,
        entityId: resourceId
      },
      context: getContextMap(event),
    };

    const authResponse = await verifiedpermissions.isAuthorizedWithToken(input);
    console.log('Decision from AVP:', authResponse.decision);
    let principalId = `${parsedToken.iss.split('/')[3]}|${parsedToken.sub}`;
    if (authResponse.principal) {
      const principalEidObj = authResponse.principal;
      principalId = `${principalEidObj.entityType}::"${principalEidObj.entityId}"`;
    }

    return {
      principalId,
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'execute-api:Invoke',
            Effect: authResponse.decision.toUpperCase() === 'ALLOW' ? 'Allow' : 'Deny',
            Resource: event.methodArn
          }
        ]
      },
      context: {
        actionId,
      }
    }
  } catch (e) {
    console.log('Error: ', e);
    return {
      principalId: '',
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'execute-api:Invoke',
            Effect: 'Deny',
            Resource: event.methodArn
          }
        ]
      },
      context: {}
    }
  }
}

module.exports = {
  handler,
};

ユーザー認証し、アクセストークンを使って API にアクセスしてみる

さて、最後に Cognito ユーザーで API へアクセスしてみましょう。
まず、自動作成されたカスタムオーソライザーを API へ設定してデプロイしておきます。

これによって、認証されていないユーザーではそもそも API へアクセスが出来なくなりましたね。

% curl https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/fuga
{"message":"Unauthorized"}
% curl https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/hoge
{"message":"Unauthorized"}

まずはどちらの API へもアクセス許可されているユーザーAで動作確認してみましょう。
Cognito に認証して、取得されたアクセストークンを使ってそれぞれの API を呼び出しています。

% hogetoken=`aws cognito-idp initiate-auth --cli-input-json file://initiate-auth.json --profile hoge | jq -r '.AuthenticationResult.AccessToken'`
% curl -H "Authorization:$hogetoken" https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/hoge                                   
hoge!!!
% curl -H "Authorization:$hogetoken" https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/fuga                                  
fuga!!!

ああ、良いですね。良いですよ。

続いてユーザーBです。こちらは hoge リソースだけ許可されて、fuga リソースは拒否されるグループに所属しています。どうなるかな。

% hogetoken=`aws cognito-idp initiate-auth --cli-input-json file://initiate-auth.json --profile hoge | jq -r '.AuthenticationResult.AccessToken'`
% curl -H "Authorization:$hogetoken" https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/hoge                                  
hoge!!!
% curl -H "Authorization:$hogetoken" https://zirh730e7a.execute-api.ap-northeast-1.amazonaws.com/hogestage/fuga                                  
{"Message":"User is not authorized to access this resource with an explicit deny"}

hoge は許可されて、fuga は拒否されましたね。
こちらも期待どおりです。

さいごに

本日は Amazon Verified Permission で既存の API Gateway + Cognito 環境を対象に認可ロジックやポリシーストアを自動作成出来るセットアップオプションが提供されるようになったので、実際に使ってみました。

これはかなり良いんじゃないでしょうか。かなり Verified Permissions を導入しやすくなったと思います。
API Gateway + Cognito 限定ではありますが、是非これを機に Verified Permissions によるポリシーストアの導入を検討してみてください。