Salesforceのサーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フローとJWT ベアラーフローの併用を試してみる

# 1. やってみたこと

Salesforceのサーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フローで`private_key_jwt`方式を利用できるか確認する

## 1.1. 背景など

[OAuth 2.0 クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)
こちらの記事を参考にすると、OAuth 2.0 クライアント認証には以下の方式があるようです
- client_secret_post
  - 認可サーバが生成した`client_id`と`client_secret`をクライアントに提供する
  - クライアントはトークンリクエストする際に`client_id`と`client_secret`を含めて送信する
- client_secret_basic
  - 認可サーバが生成した`client_id`と`client_secret`をクライアントに提供する
  - クライアントがトークンリクエストする際にBasic認証を使用して送信する
  - `client_id:client_secret`をbase64エンコードしたものを`Authorization`ヘッダーに埋め込む
- client_secret_jwt
  - 認可サーバが生成した`client_id`と`client_secret`をクライアントに提供する
  - クライアントがトークンリクエストする際にJWTを作成して、それを含めて送信する
  - JWTを生成する際に`client_secret`を使用して署名する
- private_key_jwt
  - 認可サーバが生成した`client_id`をクライアントに提供する
  - クライアントで秘密鍵と署名証明書を作成します
  - クライアントで作成した署名証明書を認可サーバに提供する
  - クライアントがトークンリクエストする際にJWTを作成して、それを含めて送信する
  - JWTを生成する際に秘密鍵を使用して署名する

[サーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5)
こちらのSalesforceマニュアルでは、`client_secret_post`と`client_secret_basic`の方法について記載があります

ただし、`client_id`と`client_secret`へのアクセス権を持つすべてのユーザまたはアプリケーションはアクセストークンを取得することができます
[Salesforceマニュアル](https://help.salesforce.com/s/articleView?id=sf.connected_app_client_credentials_setup.htm&type=5)では、そのようなセキュリティリスクを考慮してコンシューマの秘密(`client_secret`)を定期的に変更することを推奨しています

コンシューマの秘密の変更を行うと、そのタイミングでクライアントはアクセストークンを取得できない期間がどうしても発生してしまいます
それであればそもそも`client_secret`を使用しない方法の方が幾分かリスクは減り、運用コストも軽減するのではないかと思います

Salesforce側の接続アプリケーションの`client_id`と、クライアント側の秘密鍵に同時にアクセスすることは困難であると思いますので、JWTベアラーフローを検討したいところです

上記を踏まえて、クライアントログイン情報フローで`private_key_jwt`方式が使用できるのかを見てみたいと思いました


# 2. バージョンなど

- Summer '23 Patch 10.8 (2023/7/3時点)
- sfdx-cli/7.206.6 darwin-x64 node-v18.15.0
```shell
% sfdx version
sfdx-cli/7.206.6 darwin-x64 node-v18.15.0
```
- スクラッチ組織で試してみる
- JWTの作成にJava, Mavenを使用します
- Java
```shell
% java --version
openjdk 17.0.7 2023-04-18
OpenJDK Runtime Environment Temurin-17.0.7+7 (build 17.0.7+7)
OpenJDK 64-Bit Server VM Temurin-17.0.7+7 (build 17.0.7+7, mixed mode, sharing)
```
- Maven
```shell
% mvn --version
Apache Maven 3.9.3 (21122926829f1ead511c958d89bd2f672198ae9f)
```

# 3. 試したこと

[OAuth 2.0 クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)
[サーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)
こちらを参照しながら試していきます

## 3.1. 確認する内容

[OAuth 2.0 クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)
こちらを参照すると、JWT要求セットの要素は以下のように記載されていました
- JWT要求セットの要素
  - iss: `client_id`
  - sub: `client_id`
  - aud: `token endpoint`

[サーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)
SalesforceのJWTベアラーフローのマニュアルでは、JWT要求セットの要素は以下のように設定すると記載されていました
- JWT要求セットの要素
  - iss: `client_id` (コンシューマ鍵)
  - sub: 接続するユーザー名
  - aud: 認証サーバの URL
    - `https://login.salesforce.com` または `https://test.salesforce.com`
    - 本番環境では前者、テスト環境(SandboxやScratch)では後者になります

`sub`に設定する値がキーに思います
`sub`に設定する値について以下のケースを試してみます

### Case 1) `sub` が `client_id` である場合

以下の組み合わせを試します
- sub
  - `client_id`
- aud
  - `https://test.salesforce.com`

推測としては、`sub`が`client_id`だとどこに接続して良いのか分からない感じがします


### Case 2) `sub` が クライアントログイン情報フローで別のユーザとして実行に設定されているユーザのユーザー名である場合

SalesforceのJWTベアラーフローでは、`sub`に接続ユーザのユーザー名を設定するので、このケースを試してみます
以下の組み合わせを試します
- sub
  - クライアントログイン情報フローで別のユーザとして実行に設定されているユーザのユーザー名
- aud
  - `https://test.salesforce.com`

### Case 3) `sub` が クライアントログイン情報フローで別のユーザとして実行に設定されているユーザとは異なるユーザのユーザー名である場合

他のユーザー名を指定した場合はどうなるかを試してみます
以下の組み合わせを試します
- sub
  - クライアントログイン情報フローで別のユーザとして実行に設定されているユーザとは異なるユーザのユーザー名
- aud
  - `https://test.salesforce.com`


## 3.2. クライアントログイン情報フロー用の接続アプリケーションの作成とアクセストークン取得の確認

まずは、クライアントログイン情報フローでアクセストークンが取得できる環境を準備します

[Salesforceのサーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フローを試してみる](https://qiita.com/peko_kun/items/dfbe0b4dd6c9d352638b)
こちらを参照して環境を準備して正常にアクセストークンを取得できることを確認します

## 3.3. JWTベアラーフローのための準備

[Salesforceのサーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフローを試してみる](https://qiita.com/peko_kun/items/3f84aee9d38f82ac2b54)
こちらを参照してJWTベアラーフローのための準備を行います

クライアントログイン情報フローでは、接続アプリケーションの`OAuth ポリシー > 許可されているユーザ`は`すべてのユーザは自己承認可能`で動作していたのでこの設定はこのままにしておきます
接続アプリケーションのプロファイルもここでは設定しないままにしておきます

JWTの作成やアクセストークンの取得方法についても上記記事の内容を利用します


## 3.4. `sub` が `client_id` である場合

JWTのJSON要求セットは以下のような形式です
```json:JWTのJSON要求セット
{
  "iss": client_id, 
  "sub": client_id, 
  "aud": "https://test.salesforce.com", 
  "exp": "1333685628"
}
```

アクセストークンを取得しようとすると以下のようなレスポンスが返ってきました
承認されていないという旨のメッセージです
```json:response
{
  "error": "invalid_grant",
  "error_description": "user hasn't approved this consumer"
}
```

`user hasn't approved this consumer`で検索すると以下のページがヒットしました

[{ "error" : "invalid_grant", "error_description" : "user hasn't approved this consumer"}](https://developer.salesforce.com/forums/?id=906F0000000kFTPIA2)
[Salesforce JWT User Hasn't Approved This Consumer (Again)](https://salesforce.stackexchange.com/questions/184363/salesforce-jwt-user-hasnt-approved-this-consumer-again)

また、こちらの記事の内容も必要そうです
[エラー「失敗: アクセスが承認されていません」](https://help.salesforce.com/s/articleView?id=000386708&type=1)
> 該当する接続アプリケーションの詳細 ([設定] | [アプリケーションを管理する] | [接続アプリケーション]) に移動すると、[OAuth ポリシー] セクションで [許可されているユーザー] が [管理者が承認したユーザーは事前承認済み] に設定されている可能性があります。
1.特定のユーザーグループにこのアプリへのアクセスを許可する手順は次のとおりです。
[プロファイルを管理する] とプロファイル関連リストを活用して、該当する Salesforce プロファイル (および関連するユーザー) へのアクセスを承認します。


接続アプリケーションに以下の設定を行います
- `OAuth ポリシー > 許可されているユーザ`を`管理者が承認したユーザは事前承認済み`に設定
- `プロファイル`に`クライアントログイン情報 > 別のユーザとして実行`に設定されているユーザーのプロファイル

もう1度アクセストークンを取得を試してみます

以下のようなレスポンスが返ってきました
```json:response
{
  "error": "invalid_grant",
  "error_description": "user hasn't approved this consumer"
}
```

う〜ん、だめそうです。。。


### 3.5. Case 2) `sub` が クライアントログイン情報フローで別のユーザとして実行に設定されているユーザのユーザー名である場合

JWTのJSON要求セットは以下のような形式です
```json:JWTのJSON要求セット
{
  "iss": client_id, 
  "sub": クライアントログイン情報フローで別のユーザとして実行に設定されているユーザのユーザー名, 
  "aud": "https://test.salesforce.com", 
  "exp": "1333685628"
}
```

3.4.で設定した`OAuth ポリシー > 許可されているユーザ`や`プロファイル`は設定したままにします

アクセストークンを取得しようとすると以下のようなレスポンスが返ってきました
```json:response
{
  "access_token": "******",
  "scope": "api",
  "instance_url": "https://MyDomain.my.salesforce.com",
  "id": "https://test.salesforce.com/id/******/******",
  "token_type": "Bearer"
}
```

正常にアクセストークンを取得することができました


### 3.6. Case 3) `sub` が クライアントログイン情報フローで別のユーザとして実行に設定されているユーザとは異なるユーザのユーザー名である場合

続いて、他のユーザーでアクセストークンを取得しようとするとどうなるのか見てみます

アクセストークンを取得しようとすると以下のようなレスポンスが返ってきました
```json:response
{
  "access_token": "******",
  "scope": "api",
  "instance_url": "https://MyDomain.my.salesforce.com",
  "id": "https://test.salesforce.com/id/******/******",
  "token_type": "Bearer"
}
```

正常にアクセストークンを取得することができました

普通にJWTベアラーフローが動作したような感じです


### 3.7. Case 4) クライアントログイン情報フローでアクセストークンを取得してみる

接続アプリケーションには、クライアントログイン情報フローとJWTベアラーフローの両方の設定がある状態です
この状態でクライアントログイン情報フローでアクセストークンを取得しようとするとどうなるか見てみます

アクセストークンを取得しようとすると以下のようなレスポンスが返ってきました
```json:response
{
  "access_token": "******",
  "signature": "******",
  "scope": "api",
  "instance_url": "https://MyDomain.my.salesforce.com",
  "id": "https://test.salesforce.com/id/******/******",
  "token_type": "Bearer",
  "issued_at": "1688369423081"
}
```

正常にアクセストークンを取得することができました


### 3.8. 接続ユーザーのログイン履歴を見てみると

- `設定 > ユーザ > ユーザ`で該当ユーザを選択します
- `ログイン履歴`をクリックします

ここを見てみると、`ログインサブ種別`に`OAuth クライアントログイン情報`となっているものとそうでないものがあります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/302eda08-7359-43c6-0da0-60f097ec524e.png)

つまり、クラアインログイン情報フローでリクエストすると`ログインサブ種別`に`OAuth クライアントログイン情報`になり、JWTベアラーフローであれば`ログインサブ種別`は空欄になるようです

クライアントログイン情報フローとJWTベアラーフローの併用ができることが分かりました


# 4. まとめると...

- クライアントログイン情報フロー
  - `client_secret_post`、`client_secret_basic`の方式を利用できる
  - `client_secret_jwt`の方式は未検証のため不明
  - `private_key_jwt`の方式は利用不可
- 接続アプリケーション
  - クライアントログイン情報フローとJWTベアラーフローの併用した設定ができる
  - クライアントログイン情報フローでリクエストすると、`クライアントログイン情報 > 別のユーザとして実行`に設定したユーザーで実行される
  - JWTベアラーフローでは、他のユーザーも利用することができる


# 参考

[OAuth 2.0 クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)
[サーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5)
[サーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)
[Salesforceのサーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フローを試してみる](https://qiita.com/peko_kun/items/dfbe0b4dd6c9d352638b)
[Salesforceのサーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフローを試してみる](https://qiita.com/peko_kun/items/3f84aee9d38f82ac2b54)
[{ "error" : "invalid_grant", "error_description" : "user hasn't approved this consumer"}](https://developer.salesforce.com/forums/?id=906F0000000kFTPIA2)
[Salesforce JWT User Hasn't Approved This Consumer (Again)](https://salesforce.stackexchange.com/questions/184363/salesforce-jwt-user-hasnt-approved-this-consumer-again)
[エラー「失敗: アクセスが承認されていません」](https://help.salesforce.com/s/articleView?id=000386708&type=1)


おわり。