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

# 1. やってみたこと

- Salesforceのサーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フローで認証を行う
- 適当なSalesforce APIにアクセスしてデータ連携を行う

# 2. バージョンや背景など

- Summer '23 Patch 10.8 (2023/6/30時点)
- 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
```
- スクラッチ組織で試してみる

# 3. 試したこと

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

## 3.1. スクラッチ組織を作成する

以下のようなコマンドでスクラッチ組織を作成します
デフォルトのDevHub組織が既に作成および認証済みとします

```shell
sfdx org create scratch --edition developer --alias demo
```
`Your scratch org is ready.`というメッセージが表示され、`sfdx org list`で作成したスクラッチ組織を確認できます

以下のコマンドで作成したスクラッチ組織をブラウザで表示します
```shell
sfdx org open -o demo
```

## 3.2. OAuth 2.0 クライアントのログイン情報フローの接続アプリケーションを設定する

OAuth 2.0 クライアントのログイン情報フローの接続アプリケーションを作成します

[OAuth 2.0 クライアントログイン情報フローの接続アプリケーションの設定](https://help.salesforce.com/s/articleView?id=sf.connected_app_client_credentials_setup.htm&type=5)
こちらを参照しながら作成します

### 3.2.1. 新規接続アプリケーションの作成

- `設定 > アプリケーション > アプリケーションマネージャ`で`新規接続アプリケーション`をクリックします

### 3.2.2. 基本情報の入力

基本情報を入力します

- `接続アプリケーション名`, `API 参照名`, `取引先責任者 メール`を設定します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/6644cda1-369d-3aac-796d-c1321bb6f0b9.png)

### 3.2.3. OAuth設定の入力

OAuth設定を入力します

- `OAuth 設定の有効化`にチェックします
- `コールバック URL`を入力します
  - クライアントログイン情報フローでは使用しない設定のため、任意のもので大丈夫です。ここでは`https://localhost:19999`としました
- `選択した OAuth 範囲`を選択します
  - 今回はSalesforce APIを利用するため、`API を使用してユーザデータを管理 (api)`を選択します
  - クライアントログイン情報フローには、リフレッシュトークンはないため、`いつでも要求を実行 (refresh_token, offline_access)`を選択する必要はありません
- `Web サーバフローの秘密が必要`をチェックします
- `更新トークンフローの秘密が必要`は不要のため、チェック不要です
- `クライアントログイン情報フローを有効化`をチェックします
　　- `コンシューマキーとコンシューマの秘密を持つ全員が、選択されたユーザの代理で組織にアクセスできます。`というダイアログが表示されるので、`OK`をクリックします
- `すべてのトークンを調査`をチェックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/bd1e60b9-9fb5-39c8-963a-38dc568b59f9.png)

### 3.2.4. 設定の保存

- `保存`します

## 3.3. 接続ユーザーの作成

接続ユーザーはAPI限定ユーザーである必要があります
 「Salesforce Integration」ライセンスの「Salesforce API Only System Integrations」 というプロファイルでAPI限定ユーザーを作成することができます
このライセンスを利用するとSalesforceライセンスを消費することがありません

- `設定 > ユーザ > ユーザ`で`新規ユーザ`をクリックします
- `ユーザライセンス`に`Salesforce Integration`を設定します
- `プロファイル`に`Salesforce API Only System Integrations`が自動的に設定されます
- `保存`して、ユーザを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/a96fc75f-1baa-0d22-97e6-fb977ffec7e2.png)


## 3.4. 接続アプリケーションにフローの実行ユーザーを設定

接続アプリケーションで認証した際に実行されるユーザーを設定します

- `設定 > アプリケーション > アプリケーションマネージャ`の該当アプリケーションの右端▼から`Manage`をクリックします
- `ポリシーを編集`をクリックします
- `クライアントログイン情報フロー > 別のユーザとして実行`に3.3. 接続ユーザーの作成で作成したユーザを設定します
- `保存`します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/e9565b04-db01-bcd7-63e5-7a720d61651f.png)

## 3.5. アクセストークンを要求する

アクセストークンを要求するためには以下のようなリクエストを送信する必要があります
```
POST /services/oauth2/token HTTP/1.1
Host: MyDomainName.my.salesforce.com
grant_type=client_credentials&
client_id=*******************&
client_secret=*******************
```

上記の`client_id`、`client_secret`は接続アプリケーションから確認することができます

### 3.5.1. `client_id`、`client_secret`の確認

- `設定 > アプリケーション > アプリケーションマネージャ`の該当アプリケーションの右端▼から`参照`をクリックします
- `API (OAuth 設定の有効化) > コンシューマキーと秘密`の`コンシューマの詳細を管理`をクリックします
- 確認コードを要求されたら入力して、`検証`をクリックします
- `コンシューマ鍵`と`コンシューマの秘密`が表示されます
- `コンシューマ鍵`が`client_id`、`コンシューマの秘密`が`client_secret`に該当します

### 3.5.2. アクセストークンの取得

以下のようなコマンドで、アクセストークンを取得することができます

```shell:requst
curl -X POST 'https://MyDomainName.my.salesforce.com/services/oauth2/token?grant_type=client_credentials&client_id=******&client_secret=******'
```

そうすると以下のようなレスポンスが返ってきます
`access_token`の値を使用してAPIにアクセスを行うことができます
```json:response
{
  "access_token": "******",
  "signature": "DnUNtnrNqLdwx4qPha05lg2dFBjRbm2SXydaDqOP/wM=",
  "scope": "api",
  "instance_url": "https://MyDomainName.my.salesforce.com",
  "id": "https://test.salesforce.com/id/******/******",
  "token_type": "Bearer",
  "issued_at": "1688110510621"
}
```

また、client_idおよびclient_secretをURLに埋め込まない方式の例は以下になります
HTTPヘッダー`Authorization`に`Basic ${client_id:client_secretをbase64エンコードしたもの}`を設定します

```shell:client_id:client_secretをbase64エンコード
echo ${client_id}:${client_secret} | base64
```

```shell:request (2)
curl -X POST -H 'Authorization: Basic ${base64encoded}' 'https://MyDomainName.my.salesforce.com/services/oauth2/token?grant_type=client_credentials'
```
こちらも先程のレスポンスと同じものが返ってきます

クライアントログイン情報フローには、リフレッシュトークンはありません
セッションタイムアウト内で取得されるアクセストークンは同じものになります


## 3.6. GraphQL APIにリクエストしてみる

ここではGraphQL APIにリクエストして、その結果を取得できるかを試してみます

取引先の`ID`、`Name`を取得するクエリーとします

### 3.6.1. 接続ユーザーに権限を付与

#### 3.6.1.1. 権限セットライセンスの割り当て

接続ユーザーはAPI限定ユーザーであり、権限も限定されています
接続ユーザーに権限セットライセンス`Salesforce API Integration`を割り当てることで取引先などへの権限を付与することができるようになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/af3a591c-14b2-2a10-3e85-eb5764d121a0.png)


#### 3.6.1.2. 権限セットの作成

必要な権限は権限セットで付与することになります

- `設定 > ユーザ > 権限セット`で`新規`をクリックします
- `表示ラベル`、`API 参照名`を入力します
- `保存`します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/f4dce865-130f-0c2f-1aaa-c78d2588adb3.png)

- `オブジェクト設定 > 取引先`で`編集`をクリックします
- `オブジェクト権限 > 参照`の`有効`をチェックします
- `ID`、`Name`(取引先名)はデフォルトで参照可能のため、ここでは設定不要です
- `保存`します

#### 3.6.1.3. 権限セットの割り当て

- `設定 > ユーザ > ユーザ`で該当ユーザを選択します
- `権限セットの割り当て > 割り当ての編集`をクリックします
- 3.6.1.1.で作成した権限セットを選択します
- `保存`します
- `権限セットの割り当て`が以下のように表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/9c42a853-515f-f0d5-d292-9310c8d62269.png)


### 3.6.2. GraphQL APIにリクエストしてみる

以下のようにGraphQL APIにリクエストを送ります

- ヘッダー`Authorization`に`Bearer ${access_token}`を設定します
- ヘッダー`Content-Type`に`application/json`を設定します
- リクエストボディを以下のようなファイルで用意します
```json:request_graphql.json
{
  "query": "query accounts {
    uiapi {
      query {
        Account {
          edges {
            node {
              Id
              Name {
                value
              }
            }
          }
        }
      }
    }
  }"
}
```

```shell:request
curl -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer ******' 'https://MyDomainName.my.salesforce.com/services/data/v58.0/graphql' -d @request_graphql.json
```

こんな感じの応答が取得できました、
```json:response
{
  "data": {
    "uiapi": {
      "query": {
        "Account": {
          "edges": [
            {
              "node": {
                "Id": "001N000002BQxgvIAD",
                "Name": {
                  "value": "Sample Account for Entitlements"
                }
              }
            }
          ]
        }
      }
    }
  },
  "errors": []
}
```

以上で、クライアントログイン情報フローでの、Salesforce APIの利用を試すことができました

# 4. Appendix

## 4.1. Salesforceでのリリース時期

- [OAuth 2.0 クライアントのログイン情報フローを使用したサーバ間インテグレーションの設定](https://help.salesforce.com/s/articleView?id=release-notes.rn_security_client_credentials_flow.htm&release=240&type=5)
  - Winter'23
  - 2022年10月16日（日本時間）
- [新しい Salesforce インテグレーションユーザライセンスを割り当てて API 限定アクセス権を付与](https://help.salesforce.com/s/articleView?id=release-notes.rn_api_integration_license.htm&release=242&type=5)
  - Spring'23
  > このライセンスは 2023 年 3 月 7 日 ～ 8 日の Salesforce 開発者会議 (TrailblazerDX) 中に発表され、2023 年 3 月 14 日からに使用可能になりました。

## 4.2. 「Salesforce Integration」ライセンスの「Salesforce API Only System Integrations」 プロファイルで作成したユーザーへの権限付与

プロファイルには項目レベルセキュリティ(FLS)を設定することはできそうですが、オブジェクト権限は設定できなそうでした
そのため、細かな権限付与を行うためには「3.6.1. 接続ユーザーに権限を付与」で行ったように、専用の権限セットを作成して割り当てるのが良さそうです


## 4.3. アクセストークンのタイムアウト

取得したアクセストークンにタイムアウトを設定することができます

- 接続アプリケーションのセッションタイムアウト値で指定することができます
  - `接続アプリケーション > Manage > ポリシーを編集 > セッションポリシー > タイムアウト値`で設定します
  - `--なし--`の場合はタイムアウトしません
- 接続アプリケーションのセッションタイムアウト値が`--なし--`の場合は、接続ユーザーのプロファイルの`セッションの設定 > セッションタイムアウトの開始条件`が有効になるようです

ただし、セッションタイムアウトになる前にアクセストークンを取得すると、セッションタイムアウトも伸びて、取得してからまたセッションタイムアウト値まで有効のようです

APIをリクエストするトランザクションの最初でアクセストークンを取得するようにすれば十分なのかなと思いました
```
トランザクション
 - アクセストークン取得
 - APIへのリクエスト1
 - APIへのリクエスト2
```

[Salesforce の OAuth2 トークン　有効期限の仕様が独特だった件について](https://qiita.com/hatanakaakihiro/items/4cbac5348b1adf89abae)
こちらの記事を参考にしました


## 4.4. クライアントシークレットの更新

接続アプリケーションに設定されているclient_idおよびclient_secretを更新することができます
ただし、更新している間は認証の挙動が不安定になるようですので運用には注意が必要と思われます

[接続アプリケーションのコンシューマ鍵とコンシューマの秘密の循環](https://help.salesforce.com/s/articleView?id=sf.connected_app_rotate_consumer_details.htm&type=5)
詳細はこちらを参照ください

## 4.5. ログインIPアドレスの制限

接続ユーザーのプロファイルの`ログイン IP アドレスの制限`で接続元IPアドレスの範囲を設定することができます
これが設定されている場合に、範囲外からのアクセスを防ぐことができます
要件が異なる接続元が複数ある場合は、`Salesforce API Only System Integrations`を元にしたカスタムプロファイルをそれぞれ用意して設定するのが良さそうです

# 5. And more...

[OAuth 2.0 クライアントログイン情報フローの接続アプリケーションの設定](https://help.salesforce.com/s/articleView?id=sf.connected_app_client_credentials_setup.htm&type=5)
こちらに以下のような注意事項が記載されていました
> 警告 クライアントログイン情報フローを設定する前に、そのセキュリティリスクを理解することが重要です。このフローを有効にすると、接続アプリケーションのコンシューマ鍵とコンシューマの秘密へのアクセス権を持つすべてのユーザまたはアプリケーションがアクセストークンを取得できます。コンシューマの秘密を定期的に変更し、侵害された場合にすぐに変更することで、セキュリティを維持します。「接続アプリケーションのコンシューマ鍵とコンシューマの秘密の循環」を参照してください。

接続アプリケーションのコンシューマ鍵とコンシューマの秘密を変更した場合に、認証できない期間がどうしてもできてしまうのが気になります
`client_id`、`client_secret`ではなく、JWTを使用したフローも試してみたいと思います
[サーバ間インテグレーション用の OAuth 2.0 JWT ベアラーフロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)

# 参考にした記事

[サーバ間インテグレーション用の OAuth 2.0 クライアントログイン情報フロー](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5)
[新しい Salesforce インテグレーションユーザライセンスを割り当てて API 限定アクセス権を付与](https://help.salesforce.com/s/articleView?id=release-notes.rn_api_integration_license.htm&release=242&type=5)
[OAuth2 Client Credentialsフローを使ったシンプルなSalesforceとのAPI連携](https://qiita.com/shunkosa/items/ec5fab901401c364ee7a)
[Salesforce の OAuth2 トークン　有効期限の仕様が独特だった件について](https://qiita.com/hatanakaakihiro/items/4cbac5348b1adf89abae)
[OAuth 2.0 全フローの図解と動画](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f)
[OAuth 2.0 クライアント認証](https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401)

おわり。

- - -

# 修正メモ

## 2023/07/06) `3.6.1. 接続ユーザーに権限を付与`の権限付与の方法を修正

接続ユーザーに、権限セットライセンスと権限セットを個別に割り当てする方法に修正しました