Amazon Cognito Identity Pools の認証プロバイダに Auth0 を使用するとき、Auth0 側のユーザー識別子 (sub) を IAM ポリシー内で使用する方法

## TL;DR

Auth0 のユーザー識別子（JWT の `sub` クレーム）の値には縦棒 `|` が含まれるため、IAM ポリシーの変数としてそのまま埋め込むことができない。Auth0 の Rules 機能を使用して、縦棒 `|` を別の文字に置換した新しいクレームを用意して、その値をユーザー属性にマッピングするように設定しよう。

## 背景

[Amazon Cognito Identity Pools](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/identity-pools.html)（以下、Identity Pools）は、あらかじめ認証されている何らかのアイデンティティに対して、AWS のリソースにアクセスするための一時的なクレデンシャルを発行できるサービスである。ここで「あらかじめ認証されている」と書いた通り、Identity Pools はユーザーを認証する機能を有していないため、認証プロバイダ（[Amazon Cognito User Pools](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html) や [Auth0](https://auth0.com/jp) など）と連携することが必須となる。

今回は、Auth0 を認証プロバイダとして使用し、Auth0 側のユーザー識別子（JWT の `sub` クレーム）の値を取得して、特定の S3 バケット内にある、その値を名前とするディレクトリ内に対してのみ読み書きできる権限を、それぞれのユーザーに付与しようとしたのだが、これに思いのほか苦戦してしまった。

#### 参考） Identity Pools が発行する識別子を使用することもできる

ちなみに今回の目的を達成するためには、認証プロバイダ（Auth0）側で管理されているユーザー識別子を使うのではなく、Identity Pools 側で各アイデンティティに対して割り当てられる ID（Identity ID と呼ばれる）を使用することもできる。こちらであれば、本稿で述べるような問題も起こらないのだが、認証プロバイダ側で管理されているユーザー識別子と Identity ID の紐付きをアプリケーション側で意識する必要がでてきてしまい、アプリケーション側で必要な実装量が増えてしまう。とはいえこの辺りはアプリケーションの作り次第なので、状況に合わせて適切に選択してほしい。

## 何に苦戦したか

IAM ロールと Identity Pools の設定を行い、Identity Pools から一時的なクレデンシャルを取得するために [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) API をコールしたところ、以下のエラーが発生した。

```
InvalidIdentityPoolConfigurationException: Invalid identity pool configuration. Check assigned IAM roles for this pool.
```

このエラーは多くの場合、認証されたアイデンティティに割り当てる IAM ロールの信頼関係 (Trust Relationship) で `sts:TagSession` アクションが許可されていないことにより発生するようなのだが、私の場合はそこは正しく設定されており、他に設定が間違っている箇所も見当が付かず、手も足も出なくなってしまった。

## 解決方法

Auth0 の代わりに、同様の認証機能を有するオープンソースの [Keycloak](https://www.keycloak.org/) を認証プロバイダとして使用したところ、なぜか上記のエラーは発生せず、期待通りの動作となった。Auth0 と Keycloak の違いは、JWT のペイロードの構造にしかないと考え、JWT のペイロードの構造に着目することにした。

以下は Auth0 が発行する JWT（アクセストークン）のペイロードの例である。

```json
{
  "iss": "https://YOUR_DOMAIN/",
  "sub": "google-oauth2|USER-ID",
  "aud": [
    "https://YOUR_API_ENDPOINT/",
    "https://YOUR_DOMAIN/userinfo"
  ],
  "iat": 1632543753,
  "exp": 1632550953,
  "azp": "YOUR_CLIENT_ID",
  "scope": "openid profile email"
}
```

`sub` クレームの値に縦棒 `|` が含まれる点と、`aud` クレームが配列になっている点が特徴的である。試しに、`sub` クレーム以外の適当なクレーム（ `azp` クレームなど）の値をユーザー属性にマッピングするよう Identity Pools を設定したところ、エラーが発生しなくなったため、どうやら原因は縦棒 `|` であるらしいと判明した。そこで、アクセストークンに縦棒 `|` を含まないクレームを新たに追加し、その値をユーザー属性にマッピングするよう Identity Pools を設定する方針を立てた。

### Auth0 の設定

Auth0 には [Rules](https://auth0.com/docs/rules) と呼ばれる機能があり、JavaScript のスクリプトを記述することで、アクセストークンや ID トークンの JWT のペイロードを高度にカスタマイズすることができる。

以下のスクリプトは、アクセストークンに `https://example.com/user_id` という名前のクレームを追加し、その値としてユーザー識別子の縦棒 `|` をコロン `:` に置換した文字列を設定している。なお、クレームのキー名が URL のような形式であるのは、JWT の仕様上予約されているキー名と衝突しないように、[そのような命名とすることが推奨されている](https://auth0.com/docs/security/tokens/json-web-tokens/create-namespaced-custom-claims) ためである。

```javascript
function (user, context, callback) {
  context.accessToken['https://example.com/user_id'] = user.user_id.replace('|', ':');
  return callback(null, user, context);
}
```

### Identity Pools の設定

続いて Identity Pools 側では、Authentication Provider の設定において、Attributes for access control で [**Use custom mappings**] を選択し、下図のようにユーザー属性のマッピングを設定した。これにより、IAM ポリシー中の `${aws:PrincipalTag/username}` という変数は、Auth0 が発行したアクセストークンの `https://example.com/user_id` クレームの値で置き換えられることになる。
![Screen Shot 2021-09-25 at 11.05.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/70ef850a-89fc-5b1e-0dcd-a51ded191267.jpeg)

### IAM ロールの設定

最後に IAM ロールについて、認証されたアイデンティティに割り当てる IAM ロールは以下のように設定した。この例では、`my-example-bucket` バケット内の `${aws:PrincipalTag/username}` という名前のディレクトリ（厳密には S3 にディレクトリという概念はないが便宜上ディレクトリと呼ぶ）内のオブジェクトの一覧を取得する権限と、そのディレクトリ内に対するオブジェクトのアップロード権限を付与している。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*",
        "cognito-identity:*"
      ],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-example-bucket",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["${aws:PrincipalTag/username}/*"]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-example-bucket/${aws:PrincipalTag/username}/*"
    }
  ]
}
```

また、今回のように認証プロバイダ側から提供される情報を IAM ポリシー中で使用するには、上記 IAM ロールの信頼関係 (Trust Relationship) で、`sts:TagSession` アクションを許可しておく必要があるので、忘れずに設定を行う。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "cognito-identity.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRoleWithWebIdentity",
        "sts:TagSession"
      ],
      "Condition": {
        "StringEquals": {
          "cognito-identity.amazonaws.com:aud": "YOUR_IDENTITY_POOL_ID"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}
```

以上のように設定することで、無事に当初の目的を達成することができた。

## 関連記事

https://community.auth0.com/t/jwt-token-sub-claim-format/63112