Amazon Verified Permissionsで外部IDプロバイダ（Keycloak）を使用してみる

# はじめに

本記事では、Amazon Verified Permissionsでのリクエストの認可に、外部のOIDC IDプロバイダから発行されたトークンを使用する方法を試してみます。
外部IDプロバイダの使用方法を説明した記事はあまり見かけないため、皆さんの参考になれば幸いです！

# Amazon Verified Permissionsとは

Amazon Verified Permissionsは、カスタムアプリケーション向けにスケーラブルで詳細な権限管理と認可機能を提供するサービスです。このサービスを利用することで、開発者はビジネスロジックから認可を分離し、安全かつ効率的なアプリケーション開発が可能になります。

特徴として、アクセス許可を詳細に定義するためには、Cedarポリシー言語を使用します。Cedarはオープンソースで、認可ポリシーの記述および、そのポリシーに基づく認可決定に利用されます。

また、Amazon Cognito、Amazon API Gateway、AWS IAM Identity Centerなど、他のAWSサービスと連携が可能です。これにより、ユーザー認証とリソースアクセス管理を包括的に提供できます。

IDプロバイダとしてAmazon Cognitoを使用して認証し、Amazon Verified Permissionsで認可する構成が一般的なようですが、2024年8月より外部IDプロバイダにも対応しました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/08/amazon-verified-permissions-oidc-identity-providers/

外部IDプロバイダとしては、Okta、CyberArk、Transmit Securityなどに対応しているようです。ただし、標準的なOIDCのJWT形式に従ったトークンを発行するIDプロバイダであれば問題なく機能する可能性が高いです（保証はありません）。実際に本記事で触れるKeycloakは正常に動作しました。

# 本記事でやりたいこと


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/bd8a383a-a59a-c527-4257-9024a5bca83f.png)

本記事では、上記構成を構築し、次のフローを実現します。

①Keycloakでユーザー認証を行います。

②Keycloakからアクセストークンが発行されます。発行されるトークンのペイロード部分は以下のとおりです。
```json
{
  "exp": 1734956205,
  "iat": 1734955905,
  "jti": "9bc0e395-49fb-4c97-8b44-15271bc2f97b",
  "iss": "https://xxxxx/realms/myrealm",
  "aud": "account",
  "sub": "a3db1040-8e10-4163-8bc6-55b7523d3b8f",
  "typ": "Bearer",
  "azp": "myclient",
  "sid": "5ed49497-2b7e-439e-98a2-60ed42f7ed91",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "realm_access": {
    "roles": [
      "default-roles-myrealm",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid read"
}
```

③発行されたアクセストークンをAuthorizationヘッダに追加して、APIリクエストを送信します。

④アクセストークンを利用して、Lambda Authorizerへ認可のリクエストを送ります。

⑤Amazon Verified Permissionsにより、ポリシーに基づいた認可処理を実行します。
今回は、「アクセストークンのscopeクレームにreadが含まれている場合、APIへのアクセスを許可する」というポリシーを設定します。

⑥認可に成功した場合、APIリクエストが許可され、処理が続行されます。



# 構築方法
## Keycloak (ALB + EC2)
こちらは本記事の主旨と関係ないため説明は省略します。
EC2上にKeycloakを構築し、ALB経由でアクセスできるようにしています。

:::note warn
IDプロバイダを構築する際は、以下の点に注意してください。
* Issuer URLは`https://`形式の必要があります
* IDプロバイダのディスカバリーエンドポイント（Issuer URL + `.well-known/openid-configuration`）は、Amazon Verified Permissionsから検出できる必要があります
:::


## API Gateway + API(AWS Lambda)
こちらも詳細は説明しません。
基本的なAPI GatewayとAWS Lambdaの構成になります。

モックAPIとして、適当なレスポンスを返すAWS Lambdaを作成します。
API GatewayではそのAPIへシンプルな、`/`パスのGETメソッドを作成します。

![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/18e7f3b2-fb22-d160-7268-0a46e42371e3.png)



この段階では認可の設定を入れていないので、API Gatewayへリクエストを投げると、APIのレスポンスが返ってきます。
```bash
$ curl --request GET \
  --url https://xxxx.execute-api.ap-northeast-1.amazonaws.com/sample/
{"statusCode": 200, "body": "Hello from Lambda!"}
```

## Amazon Verified Permissions

１．Amazon Verified Permissionsのトップページから、「ポリシーストアを作成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/490154ce-926d-9b76-0f2e-9314b41087b9.png)

２．起動オプションから、「API Gateway と ID プロバイダーによるセットアップ - 新品です」（新品です...？）を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/190b5509-0453-0005-4672-25e114e968c6.png)


３．リソースとアクションをインポートするために、上記で作成したAPI Gatewayをインポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/6a9a6d18-a98a-9eff-35b0-ad8b1dc8f078.png)


４．IDソースの設定をします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/158b8aa4-b414-a84a-2215-a1fcaedd5d0a.png)


以下の表に、IDソースの各設定値をまとめました。

| 設定項目                   | 設定値                                   | 説明                                                                                  |
|---------------------------|------------------------------------------|-------------------------------------------------------------------------------------|
| OpenID コネクトプロバイダの種類 | 外部 OIDC プロバイダー                          | Keycloakを使用するため。この設定を選択します。                                                   |
| OIDC プロバイダーの詳細       | `https://<Your-Keycloak-Issuer-URL>`    | IDプロバイダ（Keycloak）のIssuerの値を入力します。URLは`https://`で始まる必要があります。     |
| トークンの種類              | アクセストークン                             | アクセストークンを選択します。これはAPIリクエスト時に用いるトークンの種類です。                                        |

※その他の設定値については、今回はデフォルトの値を使用します。



５．ポリシーストアでは、グループに関連付けられるアクションを指定できます。今回はグループによる承認を行いませんが、グループの設定が必要であるため、許可されるアクションにチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/97cdc07e-7522-402c-c9eb-7c26cc13af48.png)


６．アプリ統合をデプロイします。今回は試験的に作成しているため、APIの認証を開始するタイミングを「今」に設定します。これにより、CloudFormationがLambda AuthorizerやAPI Gatewayの設定を自動的に行ってくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/ba0465c1-191f-7c73-b259-ac39215dd396.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/5ec6afa0-7971-8520-96fe-10cefc4ad4a7.png)


デプロイされるとこのような表示になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/71b76e5a-46ff-b31f-9030-15150ab94e58.png)


# ポリシーの設定
構築完了したので、認可のためのポリシーを定義していきます。

## IDプロバイダのトークンをスキーマにマッピングする
デフォルトのスキーマのままでは、トークンのクレームをポリシー定義で利用することができません。そのため、公式ドキュメントを参考にして、IDプロバイダのトークンをスキーマにマッピングします。

https://docs.aws.amazon.com/ja_jp/verifiedpermissions/latest/userguide/identity-sources_map-token-to-schema.html#identity-sources_map-access-token

１．左タブのスキーマを選択し、「スキーマを編集」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/0c9d3ddf-d3e3-ffa5-e108-b08a416145b8.png)


デフォルトで作成されているスキーマ定義を修正します。
これにより、ポリシー定義で`scope`クレームを呼び出せるようになりました。

```diff_json
{
    "MyAPI": {
        "entityTypes": {
            "User": {
                "shape": {
                    "attributes": {},
                    "type": "Record"
                },
                "memberOfTypes": [
                    "UserGroup"
                ]
            },
            "UserGroup": {
                "shape": {
                    "attributes": {},
                    "type": "Record"
                }
            },
            "Application": {
                "shape": {
                    "attributes": {},
                    "type": "Record"
                }
            }
        },
        "actions": {
            "get /": {
                "appliesTo": {
                    "context": {
-                         "type": "Record",
-                         "attributes": {}
+                         "type": "ReusedContext"
                    },
                    "principalTypes": [
                        "User"
                    ],
                    "resourceTypes": [
                        "Application"
                    ]
                }
            }
-         }
+         },
+         "commonTypes": {
+          "ReusedContext": {
+             "attributes": {
+                "token": {
+                   "type": "Record",
+                   "attributes": {
+                      "scope": {
+                         "type": "Set",
+                         "element": {
+                            "type": "String"
+                         }
+                      }
+                   }
+                }
+             },
+             "type": "Record"
+             }
+         }
    }
}
```

## ポリシーの作成

いよいよポリシーを作成します。
以下の公式ドキュメントでは、IDプロバイダのアクセストークンのクレームを参照するポリシーの作成方法が示されています。
これを参考にしながら作成していきます。

https://docs.aws.amazon.com/ja_jp/verifiedpermissions/latest/userguide/policies_examples.html#policies_examples-oidc-access

１．左タブからポリシーを選択し、「ポリシーの作成」→「静的ポリシーの作成」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/97ff9f43-6e10-efeb-5105-5e468f5ecc4d.png)


２．以下の通りポリシーの範囲を設定します。今回はアクションの範囲のみ設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/5dfd825e-918a-2a5c-349b-85e01bab945c.png)


３．JSON形式でポリシーを編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2812915/21edb1e5-1139-766b-dd1a-1e27fd2f3159.png)


`context.token.scope.contains("read")`は、アクセストークンの`scope`クレームに`read`が含まれていることを表しています。

```diff
- permit(principal, action in [MyAPI::Action::"get /"], resource);
+ permit (
+     principal,
+     action in [MyAPI::Action::"get /"],
+     resource
+ )
+ when
+ {
+     context.token.scope.contains("read")
+ };
```

これでポリシーの設定が完了です！！

# 動作確認

## 認可成功の場合

Keycloakから発行された以下のペイロードを持つアクセストークンを使用してリクエストを送信してみます。
重要なのは、`scope`に`read`が含まれている点です。
```json
{
  "exp": 1734971645,
  "iat": 1734971345,
  "jti": "f4149dd8-ac56-4e83-b327-a4a74883fca0",
  "iss": "https://m5-takeda.org/realms/myrealm",
  "aud": "account",
  "sub": "a3db1040-8e10-4163-8bc6-55b7523d3b8f",
  "typ": "Bearer",
  "azp": "myclient",
  "sid": "ffcaf936-5931-45ff-8bbb-0db17c4d2281",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "realm_access": {
    "roles": [
      "default-roles-myrealm",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid read"
}
```

APIからのレスポンスは以下の通りです。
認可に成功し、APIから200レスポンスが返ってきました。
```bash
$ curl --request GET \
  --url https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/sample/ \
  --header 'Authorization: bearer <access-token>'
{"statusCode": 200, "body": "Hello from Lambda!"}
```

## 認可失敗の場合

次は、Keycloakから発行された以下のペイロードを持つアクセストークンを使用してリクエストを送信してみます。
重要なのは、`scope`に`read`が含まれていない点です。

```json
{
  "exp": 1734971861,
  "iat": 1734971561,
  "jti": "9da9ebd4-2790-418f-8d60-41378ae298cd",
  "iss": "https://xxxxx/realms/myrealm",
  "aud": "account",
  "sub": "a3db1040-8e10-4163-8bc6-55b7523d3b8f",
  "typ": "Bearer",
  "azp": "myclient",
  "sid": "9b85c6b8-d720-438f-bbfc-06cb3a9b06b9",
  "acr": "1",
  "allowed-origins": [
    "/*"
  ],
  "realm_access": {
    "roles": [
      "default-roles-myrealm",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid"
}
```

APIからのレスポンスは以下の通りです。
認可に失敗し、エラーメッセージが返ってきました。
```bash
$ curl --request GET \
  --url https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/sample/ \
  --header 'Authorization: bearer <access-token>'
{"Message": "User is not authorized to access this resource with an explicit deny"}
```


# さいごに
本記事では、Amazon Verified Permissionsを利用した外部IDプロバイダとの連携により、認可フローを構築する方法を紹介しました。
初めてAmazon Verified Permissionsを触ってみた感想としては、CloudFormationがLambda AuthorizerやAPI Gatewayの設定を自動で構築してくれる点が特にありがたかったです。
一方で、トークンのスキーマへのマッピングまわりは、設定方法が分からず戸惑うことが多かったです。
今後はより複雑なポリシー設定も調査してみたいです！