Salesforceの接続アプリケーションのJWTベアラーフローを利用してB2C Commerce からアクセストークンを取得する

※ これから記載する事項は、私が所属する会社とは一切関係のない事柄です。


Salesforceに接続アプリケーションを作成し、「[OAuth 2.0 JWT Bearer Flow for Server-to-Server Integration](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5)」を利用してSalesforceオブジェクトにアクセスするためのアクセストークンを取得したいと思います。
「[OAuth 2.0 Username-Password Flow for Special Scenarios](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_username_password_flow.htm&type=5)」を利用しているが、クレデンシャル情報をやりとりしたくないという方にはおすすめかと思います。


## 完成形

`/JWT-Sample` にブラウザからアクセスするとアクセストークンを取得し、ブラウザに表示します。
ここで取得されたアクセストークンは[オブジェクトの情報を取得する時](https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/dome_describeGlobal.htm)などに利用できます。

![スクリーンショット 2022-02-02 11.45.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/3791cefc-e3a1-08c3-2ceb-118777ee34a4.png)






## 前提

* Business ManagerとSalesforceアカウントを持っている
* [カートリッジの取り扱い](https://qiita.com/syunyama/items/21c71ac9c49b332e126d#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E6%96%B9%E6%B3%95)について知っている



## 手順概要

1. 証明書と秘密鍵の作成
2. 証明書と秘密鍵の設定
3. サイト環境設定のインポートと設定
4. コントローラーの作成




## 手順詳細

### 1. 証明証と秘密鍵の作成

下記のOpenSSLコマンドを実行してください。(ファイル名は任意です。)


* 証明書を作成。実行時に聞かれるパスワードは適当なものを入力。
    * 実行時に発行した人の情報を聞かれるのですが、少なくとも一つは入れてください。例えば、「Country Name (2 letter code)」に「JP」と入れる。じゃないと `public.crt` は作成されません。

```sh
openssl req -x509 -newkey rsa:2048 -keyout private.key -out public.crt -days 365
```

* 秘密鍵の作成。実行時に聞かれるパスワードは上記コマンドで入力したもの。
    * -nameはなくてもいいですが、使用するライブラリによってはkeyを取得するときにエイリアスを指定する必要があります。今回はなくてもいいです

```sh
openssl pkcs12 -export -out private.p12 -inkey private.key -in public.crt -name {任意の名前}
```


上記を実行すると下記のファイルが実行したディレクトリ内に作成されます。

* private.p12
* private.key
* public.crt



### 2. 証明書と秘密鍵の設定

Salesforceの接続アプリケーションを作成し、証明書を設定していきます。

* 設定のクイック検索から「アプリケーションマネージャ」を検索してください。ページを表示したら「新規接続アプリケーション」をクリックします
* 下記の内容を入力してください
    * 接続アプリケーション名　→　任意の名前
    * API参照名　→ 任意の文字列
    * 取引先責任者メール　→　作成するアプリケーションの問い合わせ先Eメール
    * コールバックURL　→　今回はあまり意味はないのですが、必須項目なので、「https://login.salesforce.com/services/oauth2/success」と入力します。
    * デジタル署名を使用　→　チェックを入れて、1. で作成したpublic.certを選択します。
    * 選択した0Auth 範囲　→　APIを使用してユーザデータを管理、いつでも要求を実行、フルアクセス、一意のユーザ識別子にアクセス
    * Web サーバフローの秘密が必要　→ チェック
* 入力できたら「保存」ボタンで保存してください


![スクリーンショット 2022-02-02 12.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/6f6b56ee-824a-7ca2-51b8-f868456b2252.png)


* 保存後に「Manage」ボタンをクリックし、さらに「ポリシーを編集」をクリック
* 許可されているユーザを「管理者が承認したユーザは事前承認済み」に変更
* 「プロファイルを管理する」または「権限セットの管理」から後述のConnectedAppSample_subに設定するユーザのプロファイルまたは権限セットをセ設定する。

![スクリーンショット 2022-02-14 14.06.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/950aa693-f2ad-dd84-91c6-a511bc40024e.png)




B2C CommerceのBusiness Managerに秘密鍵を設定します。


* Business Managerで **管理 > オペレーション > 秘密鍵と証明書** に遷移し、「インポートボタンをクリック」しポップアップを表示。
* 1.で作成した `private.p12` をファイル選択
* 下記の内容を入力
    * エイリアス　→　任意の文字列
    * ソースパスワード　→　1.で入力したパスワード
* 入力が完了したら「保存」ボタンをクリック


![スクリーンショット 2022-02-02 12.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/d3b02e2e-4558-7870-a5a2-bc02e4f9ae21.png)




### 3.サイト環境設定のインポートと設定

カスタム環境設定を利用し、トークン取得に必要な情報を設定していきます。
カスタム環境設定は、管理 > サイトの開発 > システムオブジェクトタイプ の「SitePreferences」からも設定できますが、今回はXMLからインポートします。


* 下記のXMLを 管理 > サイトの開発 > インポート & エクスポート からアップロードし、メタデータからインポートしてください。(名前は任意のものに設定してください)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://www.demandware.com/xml/impex/metadata/2006-10-31">
    <type-extension type-id="SitePreferences">
        <custom-attribute-definitions>
            <attribute-definition attribute-id="ConnectedAppSample_aud">
                <type>string</type>
                <mandatory-flag>false</mandatory-flag>
                <externally-managed-flag>false</externally-managed-flag>
                <min-length>0</min-length>
            </attribute-definition>
            <attribute-definition attribute-id="ConnectedAppSample_iss">
                <type>string</type>
                <mandatory-flag>false</mandatory-flag>
                <externally-managed-flag>false</externally-managed-flag>
                <min-length>0</min-length>
            </attribute-definition>
            <attribute-definition attribute-id="ConnectedAppSample_keyref">
                <type>string</type>
                <mandatory-flag>false</mandatory-flag>
                <externally-managed-flag>false</externally-managed-flag>
                <min-length>0</min-length>
            </attribute-definition>
            <attribute-definition attribute-id="ConnectedAppSample_sub">
                <type>string</type>
                <mandatory-flag>false</mandatory-flag>
                <externally-managed-flag>false</externally-managed-flag>
                <min-length>0</min-length>
            </attribute-definition>
        </custom-attribute-definitions>
        <group-definitions>
            <attribute-group group-id="ConnectedAppSample">
                <attribute attribute-id="ConnectedAppSample_aud"/>
                <attribute attribute-id="ConnectedAppSample_iss"/>
                <attribute attribute-id="ConnectedAppSample_keyref"/>
                <attribute attribute-id="ConnectedAppSample_sub"/>
            </attribute-group>
        </group-definitions>
    </type-extension>
</metadata>
```

* アップロードが環境すると、**マーチャントツール > サイト環境設定 > カスタムサイト環境設定グループ** に「ConnectedAppSample」が現れます。
* ConnectedAppSampleをクリックし、下記の内容を入力してください
    * ConnectedAppSample_aud　→　[https://login.salesforce.com](https://login.salesforce.com/) (サンドボックスの場合は、https://test.salesforce.com/)
    * ConnectedAppSample_iss　→　接続アプリケーションのコンシューマ鍵（下記画像参照）
    * ConnectedAppSample_keyref　→　Business Managerに秘密鍵を設定した際のエイリアス
    * ConnectedAppSample_sub　→　このアプリケーションを利用するユーザがSalesforceログイン時にユーザ名として利用しているEメールアドレス。先述した接続アプリケーションの設定内で、OAuthポリシーを「管理者が承認したユーザは事前承認済み」として設定したプロファイル・権限セットをもったユーザ。

![スクリーンショット 2022-02-02 12.26.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/144aaa23-02a7-4fd4-b583-5d31adca252f.png)





* 入力が完了したら、「保存」ボタンをクリックしてください。

![スクリーンショット 2022-02-02 12.34.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2110894/ec691fc8-dd8b-09c2-39aa-036341197998.png)



### 4.コントローラーの作成

* 任意のカートリッジを作成し、`JWT.js` と言うコントローラを作成し、下記の内容を入力し、アップロードし、カートリッジパスを設定してください。
    * ※エラーハンドリングやコードに対するJS Docなどのコメントアウトは今回省いています。

```js
"use strict";

var server = require("server");
var Signature = require("dw/crypto/Signature");
var KeyRef = require("dw/crypto/KeyRef");
var Encoding = require("dw/crypto/Encoding");
var Bytes = require("dw/util/Bytes");
var StringUtils = require("dw/util/StringUtils");
var LocalServiceRegistry = require("dw/svc/LocalServiceRegistry");

function _createService() {
    return LocalServiceRegistry.createService("oauth.test", {
        createRequest: function (svc, jwt) {
            svc.setURL("https://login.salesforce.com/services/oauth2/token");
            svc.addHeader("Content-Type", "application/x-www-form-urlencoded");
            svc.setRequestMethod("POST");
            svc.addParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
            svc.addParam("assertion", jwt);
        },
        parseResponse: function (svc, response) {
            return JSON.parse(response.text);
        },
        filterLogMessage: function (msg) {
            return msg;
        },
    });
}

function _replaceToUrlEncodeBase64String(string) {
    return string
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/\=+$/m, '');
}

function _createJWT(){
    var headerStr = '{"alg":"RS256"}';
    var encodedHeaderStr = StringUtils.encodeBase64(headerStr);
    var iss = dw.system.Site.current.getCustomPreferenceValue("ConnectedAppSample_iss");
    var sub = dw.system.Site.current.getCustomPreferenceValue("ConnectedAppSample_sub");
    var aud = dw.system.Site.current.getCustomPreferenceValue("ConnectedAppSample_aud");
    var exp = String(Math.floor(Date.now() / 1000) + 3 * 60);
    var claimStr = StringUtils.format(
        '\'{\'"iss": "{0}", "sub": "{1}", "aud": "{2}", "exp": "{3}"\'}\'',
        iss,
        sub,
        aud,
        exp
    );
    var encodedClaimStr = StringUtils.encodeBase64(claimStr);
    var joined = encodedHeaderStr + "." + encodedClaimStr;
    var keyref = dw.system.Site.current.getCustomPreferenceValue("ConnectedAppSample_keyref");
    var privateKey = KeyRef(keyref);
    var signed = Signature().signBytes(Bytes(joined, "UTF-8"), privateKey, "SHA256withRSA");
    return joined + "." + _replaceToUrlEncodeBase64String(Encoding.toBase64(signed));
}

server.get("Sample", function (req, res, next) {
    var jwt = _createJWT();
    var service = _createService();
    var result = service.call(jwt);
    res.json({
        access_token: result.object.access_token,
        instance_url: result.object.instance_url,
    });
    next();
});

module.exports = server.exports();
```


## コントローラーのコードのポイント

* `KeyRef`で2. で設定した秘密鍵を呼び出しています。
* `Signature().signBytes`でデータに署名しています。
* `_replaceToUrlEncodeBase64String`でURLに使えない文字列を置換しています。
* `dw.system.Site.current.getCustomPreferenceValue`で3.で設定した値を読み込んでいます。