Salesforce で Client Credentials フローな指定ログイン情報を使いたい

まだコロナの影響もなく、騒いでいることのできた昨年末のAdvent Calendarで「[Salesforce で外部サービスを使うときには「指定ログイン情報」を使いましょう](https://qiita.com/sho7650/items/577e54f390bc97e8d751)」と偉そうに書いてから、もう4ヶ月近くが経過します。

確かに「[指定ログイン情報](https://help.salesforce.com/articleView?id=named_credentials_about.htm&type=5)」は便利です。`Apex`コールアウトするときも、「[外部サービス](https://help.salesforce.com/articleView?id=external_services.htm&type=5)」を利用するときも[指定ログイン情報](https://help.salesforce.com/articleView?id=named_credentials_about.htm&type=5)様様です。

しかし、その便利な「[指定ログイン情報](https://help.salesforce.com/articleView?id=named_credentials_about.htm&type=5)」も、「[認証プロバイダ](https://help.salesforce.com/articleView?id=sso_authentication_providers.htm&type=5)」があってこそです。いま対応しているプロバイダは次の10個です。

- Apple
- Facebook
- Janrain
- Salesforce
- Open ID Connect
- Microsoft アクセスコントロールサービス
- Linkedin
- Twitter
- Google
- GitHub

※ 2020/4/20 現在 (Apple増えたね)

これを見て、心穏やかにならないなにかがありませんか？そうです、「認可」として業界標準で最も利用されている「[OAuth2.0](https://oauth.net/2/)」がありません。TwitterやFacebookだってOAuthアクセスなのに、なぜ標準的なOAuth2.0は登録されていないのか。`Open ID Connect`はあるのに...。

と気が滅入った方。多分仕方ないです。OAuth2.0はその認可の方法が多岐にわたり、一つ一つを登録していく労力やらを考えると、利用者の多いサイトを認証プロバイダとして登録するほうがサービサー的には正しいかもしれません。

しかし、我々は OAuth2.0 で認可されているサイトを外部サービスとして利用したいんです。さて、どうするべきか。

この方法は、われらがデベロッパエヴァンジェリストである[@hrk623](https://qiita.com/hrk623)の書いた「[netatmo Weather Station のデータを定期的に Salesforce に取り込んでみた](https://qiita.com/hrk623/items/129eb922bb7d9557c721)」のように、長い手順を踏んだ上に「[カスタム認証プロバイダ](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/authproviderplugin.htm)」を`apex`で記述する必要があります。

なんてこった。

Salesforce はAdmin利用の多いサービスであり、ノーコード・ローコードが売りです。この部分なんとかならんかと思い、ここでわたしは一肌脱いだ次第です(前置きが長い)。

# Client Credentials をかんたんに実装しよう

そこで、外部サービスとしてシステム連携を行う際に利用する頻度の高いと思われる「[Client Credentials](https://auth0.com/docs/flows/concepts/client-credentials)」について、できるだけかんたんに実装できる「[ソースコード](https://github.com/sho7650/clientCredentialsAuthProvider)」を準備しました。しかも、カスタマイズ機能付き。

今日は、こちらの使い方を紹介します。Apexコードの解説は別途。

## Client Credential フローって？

先程の[auth0](https://auth0.com/)サイトのドキュメントが詳しいのですが、事前に認証されて取得した次の2つを利用して(1)、認可用のアクセストークンを発行してもらい(2),(3)、APIサーバへはそのアクセストークンを使ってアクセスする方式(4),(5)です。システム連携用としては、非常に多いパターンです。認証スキーム名として`Bearer`を利用する方式ですね。

1. Client ID
2. Client Secret

![auth-sequence-client-credentials.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37263/152da74d-03a7-5851-63db-1f42641a4c4d.png)
※ from https://auth0.com/docs/flows/concepts/client-credentials

## 使い方

[Github 上のソースコード](https://github.com/sho7650/clientCredentialsAuthProvider)に記載のあるとおりの手順を実行すれば、[DevHub](https://developer.salesforce.com/docs/atlas.ja-jp.sfdx_setup.meta/sfdx_setup/sfdx_setup_enable_devhub.htm)内に[スクラッチ組織](https://developer.salesforce.com/docs/atlas.ja-jp.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs.htm)を作成して、そこへ「Client Credentials」用のカスタム外部認証プロバイダを登録します。

流石に投げっぱなしすぎるので、一つずつ手順と注意事項を記載しておきます。

## 前提条件

- 組織でDevHubを利用可能としている (その他の環境へDeployする場合は不要です)
- [Salesforce CLI](https://developer.salesforce.com/ja/tools/sfdxcli) をインストールしていること
- git コマンドが利用できること

## ソースコードを準備します

[Githubのソースコード](https://github.com/sho7650/clientCredentialsAuthProvider)を、`git clone` して入手します。

```bash
git clone https://github.com/sho7650/clientCredentialsAuthProvider.git
cd ./clientCredentialsAuthProvider
```

`ls -latr`すると、こんな感じのディレクトリ構成になっていれば問題なしです。

```bash
$ ls -latr
total 56
drwxr-xr-x   3 sho  staff    96 Apr 20 10:40 ../
-rwxr-xr-x   1 sho  staff   349 Apr 20 10:40 .forceignore*
-rwxr-xr-x   1 sho  staff   597 Apr 20 10:40 .gitignore*
-rwxr-xr-x   1 sho  staff   155 Apr 20 10:40 .prettierignore*
-rwxr-xr-x   1 sho  staff   228 Apr 20 10:40 .prettierrc*
-rwxr-xr-x   1 sho  staff  4206 Apr 20 10:40 README.md*
drwxr-xr-x   4 sho  staff   128 Apr 20 10:40 bin/
drwxr-xr-x   3 sho  staff    96 Apr 20 10:40 config/
drwxr-xr-x   4 sho  staff   128 Apr 20 10:40 force-app/
drwxr-xr-x   3 sho  staff    96 Apr 20 10:40 manifest/
-rwxr-xr-x   1 sho  staff   193 Apr 20 10:40 sfdx-project.json*
drwxr-xr-x  12 sho  staff   384 Apr 20 10:40 .git/
drwxr-xr-x  14 sho  staff   448 Apr 20 10:41 ./
drwxr-xr-x   5 sho  staff   160 Apr 20 10:44 .sfdx/
```

## Salesforce 組織の認証

DevHub を利用する場合は次のとおりに行います。

- <alias your DevHub> ... 使用するDevHubのエイリアス(別名)をつけます。使いやすいようにです
- <alias your scratch Org> ... deploy先のスクラッチ組織のエイリアス(別名)をつけます。同じく使いやすくするためです

```bash
sfdx force:auth:web:login -d -a <alias your DevHub>
sfdx force:org:create -s -f config/project-scratch-def.json -a <alias your scratch Org>
```

`sfdx force:auth:web:login` すると Salesforce組織へ[Webベースフローで認証](https://developer.salesforce.com/docs/atlas.ja-jp.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_web_flow.htm)するため、Salesforce のログインが出てきます。ここで`DevHub`のある組織へログインします。次の `sfdx force:org:create` コマンドにてスクラッチ組織を作成します。

もしここで、スクラッチ組織ではなくログイン先のDevloper、Sandbox、本番環境へdeployする場合にはスクラッチ組織を作らなければdeploy可能です(多分)。

## ソースコードのpushとテスト

では、手元にあるソースコードを Salesforce へ `push`(アップロード) しましょう。

```bash
sfdx force:source:push
```

何事もなければ、次のようにファイルがすべて登録されます。

```bash
=== Pushed Source
STATE  FULL NAME                                                          TYPE               PROJECT PATH
─────  ─────────────────────────────────────────────────────────────────  ─────────────────  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Add    ClientCredentials                                                  AuthProvider       force-app/main/default/authproviders/ClientCredentials.authprovider-meta.xml
Add    ClientCredentialsAuthProvider                                      ApexClass          force-app/main/default/classes/ClientCredentialsAuthProvider.cls
Add    ClientCredentialsAuthProvider                                      ApexClass          force-app/main/default/classes/ClientCredentialsAuthProvider.cls-meta.xml
Add    ClientCredentials_Metadata.ClientCredentials                       CustomMetadata     force-app/main/default/customMetadata/ClientCredentials_Metadata.ClientCredentials.md-meta.xml
Add    ClientCredentials_Metadata__mdt-ClientCredentials_Metadata Layout  Layout             force-app/main/default/layouts/ClientCredentials_Metadata__mdt-ClientCredentials_Metadata Layout.layout-meta.xml
Add    ClientCredentials                                                  NamedCredential    force-app/main/default/namedCredentials/ClientCredentials.namedCredential-meta.xml
Add    ClientCredentials_Metadata__mdt                                    CustomObject       force-app/main/default/objects/ClientCredentials_Metadata__mdt/ClientCredentials_Metadata__mdt.object-meta.xml
Add    ClientCredentials_Metadata__mdt.Access_Token_URL__c                CustomField        force-app/main/default/objects/ClientCredentials_Metadata__mdt/fields/Access_Token_URL__c.field-meta.xml
Add    ClientCredentials_Metadata__mdt.Auth_Provider__c                   CustomField        force-app/main/default/objects/ClientCredentials_Metadata__mdt/fields/Auth_Provider__c.field-meta.xml
Add    ClientCredentials_Metadata__mdt.Client_ID__c                       CustomField        force-app/main/default/objects/ClientCredentials_Metadata__mdt/fields/Client_ID__c.field-meta.xml
Add    ClientCredentials_Metadata__mdt.Client_Secret__c                   CustomField        force-app/main/default/objects/ClientCredentials_Metadata__mdt/fields/Client_Secret__c.field-meta.xml
Add    ClientCredentials_Metadata__mdt.User_Name__c                       CustomField        force-app/main/default/objects/ClientCredentials_Metadata__mdt/fields/User_Name__c.field-meta.xml
Add    clientCredentials                                                  RemoteSiteSetting  force-app/main/default/remoteSiteSettings/clientCredentials.remoteSite-meta.xml
Add    ClientCredentialsAuthProviderTest                                  ApexClass          force-app/test/default/classes/ClientCredentialsAuthProviderTest.cls
Add    ClientCredentialsAuthProviderTest                                  ApexClass          force-app/test/default/classes/ClientCredentialsAuthProviderTest.cls-meta.xml
```

無事に登録された場合には、テストも可能です。テストコードも実行してみましょう。

```bash
sfdx force:apex:test:run --tests ClientCredentialsAuthProviderTest --resultformat human --outputdir .sfdx/tools/testresults/apex --loglevel error --codecoverage
```

全体で6つのテストケースがありますので、問題がなければすべて次のように全てパスします。

```bash
=== Test Reports
FORMAT  FILE
──────  ──────────────────────────────────────────────────────────────────
txt     .sfdx/tools/testresults/apex/test-result.txt
txt     .sfdx/tools/testresults/apex/test-run-id.txt
junit   .sfdx/tools/testresults/apex/test-result-7075D000018Sj4b-junit.xml
json    .sfdx/tools/testresults/apex/test-result-7075D000018Sj4b.json
json    .sfdx/tools/testresults/apex/test-result-codecoverage.json

=== Apex Code Coverage
ID                  NAME                           % COVERED  UNCOVERED LINES
──────────────────  ─────────────────────────────  ─────────  ───────────────
01p5D0000013JboQAE  ClientCredentialsAuthProvider  100%

=== Test Results
TEST NAME                                                    OUTCOME  MESSAGE  RUNTIME (MS)
───────────────────────────────────────────────────────────  ───────  ───────  ────────────
ClientCredentialsAuthProviderTest.getCustomMetadataTypeTest  Pass              23
ClientCredentialsAuthProviderTest.getUserInfoTest            Pass              6
ClientCredentialsAuthProviderTest.handleCallbackErrorTest    Pass              6
ClientCredentialsAuthProviderTest.handleCallbackTest         Pass              5
ClientCredentialsAuthProviderTest.initiateTest               Pass              5
ClientCredentialsAuthProviderTest.refreshTest                Pass              5

=== Test Summary
NAME                 VALUE
───────────────────  ──────────────────────────────────────────────────────────
Outcome              Passed
Tests Ran            6
Passing              6
Failing              0
Skipped              0
Pass Rate            100%
Fail Rate            0%
Test Start Time      Apr 20, 2020 10:44 AM
Test Execution Time  50 ms
Test Total Time      50 ms
Command Time         1783 ms
Hostname             https://java-business-3629-dev-ed.cs72.my.salesforce.com/
Org Id               00D5D000000DRVEUA4
Username             test-clientcredentials@example.com
Test Run Id          7075D000018Sj4b
User Id              0055D000003BGPDQA4
Test Run Coverage    100%
Org Wide Coverage    100%
```

これで、コードは登録できましたので、あとは次の2つを相手の認証・認可サーバに必要な設定を行えば設定完了です。

- [認証プロバイダ](https://help.salesforce.com/articleView?id=sso_authentication_providers.htm&type=5)
- [カスタムメタデータ型](https://help.salesforce.com/articleView?id=custommetadatatypes_overview.htm&type=5)
- [指定ログイン情報](https://help.salesforce.com/articleView?id=named_credentials_about.htm&type=5)
- [リモートサイト](https://help.salesforce.com/articleView?id=configuring_remoteproxy.htm&type=5)

## どうせなら、カスタマイズしたままdeployしたい

そうでしょう。そうでしょう。どうせなら、コードを`push`した時にすべての構成が反映している方が望ましいです。分かります、その気持ち。

はい、準備しました。次のコマンドを実行するだけです。

```bash
./bin/config.sh <your config>
```

※ macOSかLinuxで実行すること。WindowsユーザはWSL上で実行してください

とは言え、このままだと何も起きません、残念な結果になります。まずは設定ファイルを準備しましょう。

次のようなファイルを作成します。`.config` というファイル名にすれば設定ファイル名を指定しなくとも自動的に読み込みます。

```bash
ENDPOINT='https://test.example.com'
EXECUTION_USER='test@example.com'
REMOTE_SITE_URL='https://api.example.com'
ACCESS_TOKEN_URL='https://example.com/token'
CLIENT_ID='client_id'
CLIENT_SECRET='client_secret'
```

環境変数名 | 内容 | デフォルト値
---|---|---
ENDPOINT | [指定ログイン情報](https://help.salesforce.com/articleView?id=named_credentials_about.htm&type=5)で指定するコールアウト先のエンドポイント| https://test.example.com
EXECUTION_USER | [認証プロバイダ](https://help.salesforce.com/articleView?id=sso_authentication_providers.htm&type=5)に登録される実行ユーザ名。実際に登録されているシステムユーザを指定しましょう | test-clientcredentials@example.com
REMOTE_SITE_URL | [リモートサイト](https://help.salesforce.com/articleView?id=configuring_remoteproxy.htm&type=5)へ登録するリモートサイトのホスト名 | https://api.example.com
ACCESS_TOKEN_URL | [カスタムメタデータ型](https://help.salesforce.com/articleView?id=custommetadatatypes_overview.htm&type=5)に登録されているアクセストークンを発行するリモートサイトのURL|https://example.com/token
CLIENT_ID | [カスタムメタデータ型](https://help.salesforce.com/articleView?id=custommetadatatypes_overview.htm&type=5) に登録されている、外部サービスから発行された `Client_ID`| client_id
CLIENT_SECRET | [カスタムメタデータ型](https://help.salesforce.com/articleView?id=custommetadatatypes_overview.htm&type=5)に登録されている、外部サービスから発行された`Client_SECRET`|client_secret

これらを指定したファイルを準備して、`./bin/config.sh` を実行すると該当のソースコード部分が全て置き換わります。書き換え後の情報は機密情報になるので、そのまま `github` などのオープンリポジトリへは`push`しないように気をつけましょう。

それでは、より良いハック人生を。