Apex Wrapper SalesforceメタデータAPI（apex-mdapi）で指定ログイン情報の使用

\:cloud: Metadata API

原文：https://www.gscloudsolutions.com/blogpost/Using-Named-Credentials-with-the-Apex-Wrapper-Salesforce-Metadata-API-apex-mdapi?blogpost=true&utm_source=twitter&utm_medium=social&utm_campaign=named_credentials
の意訳です。(原文は2017/8/28です、その後に設定場所が変わっているものもあるので2020/10/29時点の表現に変えています。また日本語環境にできるだけ合わせています）


Andrew Fawcett @FinancialForceによってリリースされたApexメタデータラッパーは、SalesforceメタデータAPIのパワーを開発者にもたらし、使い慣れたオンプラットフォーム言語を使用してSalesforceメタデータを操作できるようにします。

よくある質問の1つは、[バッチジョブまたはファーストクラスのセッションIDが利用できない他のシナリオ内からこのAPIを使用する方法です。](https://github.com/financialforcedev/apex-mdapi/issues/89)従来の解決策は、login（）SOAP APIメソッドを使用して認証するか、Apexで手動でOAuthフローを実装することでした。これには、Apex（非常に問題な方法）またはカスタム設定（少々問題な方法）にハードコードされたパスワードを保存する必要があります。これらは潜在的に「安全性が低い」。


2017年夏から、[Salesforceは、Apexからメタデータを取得および操作するための公式サポートを発表しましたが、](https://developer.salesforce.com/blogs/engineering/2017/05/introducing-apex-metadata-api.html)現時点では機能が制限されており、将来的にはより幅広いメタデータタイプがサポートされる予定です。(2017/8/28時点で現在はどうかは私は確認できてないです、訳者注)

待ちきれず、今すぐこの機能が必要な場合はどうなりますか？指定ログイン情報を活用してメタデータAPIで安全に認証する方法については、以下をお読みください。


**注：**以下の手順は、中級レベルのSalesforceの技術知識を前提としています。この投稿では、これがどのように機能するかについてはあまり詳しく説明しませんが、興味がある場合は、[Authorization Through Connected Apps and OAuth 2.0](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_oauth_and_connected_apps.htm)と[Named Credentials as Callout Endpoints](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_named_credentials.htm)についてさらに背景を読んでください。

# ステップ1：接続アプリケーションを作成する

まず、接続アプリを作成します。 [設定]で、[アプリケーションマネージャ]に移動し、[新規接続アプリケーション]ボタンをクリックします。

アプリに名前を付けます。プレースホルダーのコールバックURLを指定し（これは後で更新します）、「full」および「refresh_token、offline_access」OAuthスコープを選択していることを確認してください。後者は、セッションの有効期限が切れたときに名前付き資格情報がaccess_tokenを更新できるようにするために重要です。

**基本情報**
接続アプリケーション名:ApexMDAPI
API 参照名:ApexMDAPI
取引先責任者 メール:pei@example.com

**API (OAuth 設定の有効化)**
OAuth 設定の有効化:True
コールバック URL:https://localhost/dummy
選択した OAuth 範囲:フルアクセス(full)、ユーザに変わっていつでも要求を実行(refresh_token,offline_access)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/e791d642-f879-1191-1751-7a035f7c95d0.png)


接続されたアプリを保存した後、生成されたコンシューマーキーとコンシューマーシークレットをメモします。これらは後程必要になります。

下記は英語表示ですが日本語では**コンシューマ鍵**のところです。

![image.png](https://www.gscloudsolutions.com/servlet/rtaImage?eid=ka01B000000stN2&feoid=00N1300000ArS50&refid=0EM1B000000VTHx)

2024/12/18追加　上記のリンクが消えている
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/76bd393e-8288-8c04-f785-19d689662899.png)

このボタンをクリックすると、メールに 確認コードが送信させます。認証できるとコンシューマ鍵を表示する画面に遷移します。

# ステップ2：認証プロバイダーを作成する


次に、認証を作成します。プロバイダー。認証プロバイダーは、Salesforce組織での認証を容易にするために使用されます。
[設定]で、[多要素認証アシスタント]-> [ID] -> [認証プロパイダ]に移動します。「新規」ボタンをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/7f45e76e-bd88-d2cd-94e1-2363962dbba6.png)
2024/12時点では少々場所が変わっている感じですね

1. プロバイダタイプ Salesforceを選択します。
2. 名前 ApexMDAPI
3. URL 接尾辞 ApexMDAPI
4. 接続アプリケーションからコンシューマーキーとコンシューマの秘密(クリックして公開)を貼り付けます。
5. 前に指定したようにスコープを入力します：full refresh_tokenoffline_access
6. 下記のスクリーンショットのように、他のすべてのフィールドを空白のままにして保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/ea9c2233-ec4a-964f-237e-a4374f0e6a1b.png)

次に、接続済みアプリを正しいコールバックURLで更新する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/4ce7538d-b9a0-b0f6-d7ec-ecddcaa96c9c.png)


コールバックURLをコピーして貼り付け、前に作成した接続アプリを編集して、[OAuth]セクションの下の[コールバックURL]に貼り付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/ee1f3672-f99f-e05b-18b7-a08ba86b3d69.png)

接続されたアプリを保存します。この時点で、設定が有効になり、Salesforceのインフラストラクチャ全体に反映されるまで数分待つ必要がある場合があります。

# ステップ3：指定ログイン情報を作成する


次に、指定ログイン情報を作成します。

[設定]で、[セキュリティ]-> [指定ログイン情報]に移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/e1135c24-f604-7e4d-9225-5cc147e353f5.png)

1. 指定ログイン情報に適切な名前を付けます。後でApexコードを変更するときにこれを使用する必要があります。
2. 組織のインスタンスURLを入力します-これはインスタンスのURL（例：na1.salesforce.com）、またはマイドメインを使用している場合は完全に修飾されたマイドメインドメイン（例：mycompany.my.salesforce.com）になります
3. [ID種別]で、[**ユーザ**]を選択します。これは、すべてのメタデータAPIインタラクションを実行する1人のユーザー（「システム管理者」など）がいることを前提としています。状況が異なる場合は、これを変更してください。
4. 認証プロトコルにOAuth2.0を選択します。
5. 認証プロバイダー用に以前に作成した認証プロバイダーを選択します。
6. [範囲]で、full refresh_tokenoffline_accessを指定します。
7. [保存時に認証フローを開始する]にチェックマークを付けて保存します。
8. 認証ヘッダーを生成のチェックはそのままオンにしておきます。
9. [HTTP 本文の[差し込み項目](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_named_credentials_merge_fields.htm)を許可する]チェックボックスをオンにしてください。認証の詳細はHTTPボディの一部であり、標準のHTTP Authorizationヘッダーにはないため、これらの差し込み項目を使用してメタデータAPIを呼び出します。

error=redirect_uri_mismatch&error_description=redirect_uri%20must%20match%20configuration
というエラーがでたら接続アプリケーションでコールバックURLが正しく更新されてません。この場合は改めて入力します。次へのボタンも必ずクリックしてください。

組織にログインするように求められます。ログインすると、次のような接続済みアプリの認証画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/67ae0585-3017-001b-60a5-39c26303fad9.png)


このアプリ（アプリ）へのアクセスを許可します。完了すると、[指定ログイン情報]画面にリダイレクトされ、ステータスが[認証済みとして...]に設定されていることを確認する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/6ab8ddcb-066f-8d30-94e7-13cb19c830c4.png)

# ステップ4：MetadataService.MetadataPortとcreateService（）を変更する。

MetadataService.cls:https://github.com/financialforcedev/apex-mdapi/blob/master/apex-mdapi/src/classes/MetadataService.cls

指定ログイン情報を使用するには、MetadataService.MetadataPort  Apexクラスを変更する必要があります。

指定ログイン情報を参照するようにendpoint_x変数を見つけて変更します。

**原文(変更後)**

```
public class MetadataPort {
        // Update endpoint_x to the name of your Named Credential
        public String endpoint_x = 'callout:ApexMDAPI/services/Soap/m/38.0';
```

**2020/10/29時点のコード**

```
public class MetadataPort {
        public String endpoint_x = URL.getSalesforceBaseUrl().toExternalForm() + '/services/Soap/m/42.0';
```





これで、MetadataServiceExampleクラスで提供されるcreateService（）メソッドを変更して、指定ログイン資格情報のOAuthTokenマージフィールドを利用できます。ここに入力する必要はありませんが、サンプルコードで提供されている規則に従うと便利です。

```
// Modify createService() to use the Named Credential merge field 

   public static MetadataService.MetadataPort createService()
    {
        MetadataService.MetadataPort service = new MetadataService.MetadataPort();
        service.SessionHeader = new MetadataService.SessionHeader_element();
        // service.SessionHeader.sessionId = UserInfo.getSessionId();
        service.SessionHeader.sessionId = '{!$Credential.OAuthToken}';

        return service;
    }
```

保存して、テストします。すべてうまくいっていれば、問題なく動くはずです。


MetadataServiceクラスを変更したくない場合はどうなりますか？サービスの作成時にendpoint_x変数を設定することもできます。

```
public static MetadataService.MetadataPort createService()
    {
        MetadataService.MetadataPort service = new MetadataService.MetadataPort();
        service.endpoint_x = 'callout:ApexMDAPI/services/Soap/m/38.0';
        service.SessionHeader = new MetadataService.SessionHeader_element();
        // service.SessionHeader.sessionId = UserInfo.getSessionId();
        service.SessionHeader.sessionId = '{!$Credential.OAuthToken}';

        return service;
    }
```

これはあまりにも良さそうですよね？ **注意！** メタデータAPIには、予期しない問題を引き起こす可能性のあるいくつかの癖があります。


# ステップ5：セッションタイムアウトエラーの回避

しばらくするとセッションのタイムアウトが発生し始めるのはなぜだろうと、何時間も費やしました。メタデータAPIの動作に問題があり、トークンの更新プロセスが適切に行われないことが判明しました。

一言で言えば、セットアップ後はすべて正常に動作しているように見えますが、一定期間（通常は数時間）が経過すると、次のようなエラーが発生します。

```
Web service callout failed: WebService returned a SOAP Fault: INVALID_SESSION_ID: 
Invalid Session ID found in SessionHeader: Illegal Session. 
Session not found, missing session hash: xxxxx 
This error usually occurs after a session expires or a user logs out. 
faultcode=sf:INVALID_SESSION_ID faultactor=
```

しかし、待ってください-接続されたアプリにoffline_accessスコープとrefresh_tokenスコープを与えませんでしたか？指定ログイン情報は、有効期限が切れたときにaccess_tokenを自動的に更新するべきではありませんか？どうしたの？

Salesforceサポート（およびその後の製品管理）との話し合いの結果、[これは](https://help.salesforce.com/articleView?id=000313355&language=en_US&type=1&mode=1)修正がやや難しい予期しない「バグ」であることが判明したため、今後の参考のためにこの記事を公開しました。

根本的な原因は、歴史的な理由から、セッションの有効期限が切れると、メタデータAPIが401ではなくHTTP 500を返すため、NamedCredentialsインフラストラクチャがトークンの更新を試行することを認識しないことです。 「セッションが期限切れになったため500」と「本当に問題が発生したため500」を区別することはできません。


回避策は、最初にREST APIの1つ（/ limitsなど）を呼び出してアクセストークンを更新し、その後メタデータAPI呼び出しを続行することです。

**私は何をする必要がありますか？**

簡易な方法：セッションの有効期限が切れたときに401エラーコードを正しく返すREST APIへのコールアウトを作成します。これにより、NamedCredentialsがOAuthaccess_tokenを更新し、メタデータAPIコールアウトにマージされます。

メタデータAPIコールアウトを実行する直前に、以下のようなコードのブロックを追加します（たとえば、createService（）メソッドを変更できます）。応答を破棄するだけですが、セッションが新鮮であることを確認するにはこれで十分です。

```
String restUrl = 'callout:ApexMDAPI/services/data/v39.0/limits';
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint(restUrl);
req.setMethod('GET');
HttpResponse res = h.send(req);

// Now do your Metadata Service calls here.
```


注：もちろん、これは余分なAPI呼び出しを浪費するため、ガバナーやその他のプラットフォームの制限内にとどまるために複数のメタデータAPI呼び出しを行う場合は注意してください。


それでおしまい！楽しんでください、そしていつものように、大きな力には大きな責任が伴います。 [ApexメタデータAPIの今後の発表に](https://developer.salesforce.com/blogs/engineering/2017/05/introducing-apex-metadata-api.html)注意してください。やがて、Salesforceの公式ソリューションにより、（素晴らしい）apex-mdapiプロジェクトなどのサードパーティソリューションの廃止が可能になることを願っています。

このような難しい技術的課題の解決に取り組んでいる賢い人々のチームに参加してみませんか？[こちらの求人情報をご覧ください。](https://www.gscloudsolutions.com/jobs)

# 指定ログイン情報のエラー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/530a6ff1-a7c7-d6a1-b2e9-3d0be8f5c51b.png)

上記のエラーが出た場合は　個人の設定から　外部システムの認証設定を開いて指定ログイン情報を登録して下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328136/54db10da-79b9-3575-fd09-d89d31ea5a9c.png)


# error=redirect_uri_mismatch&error_description=redirect_uri 20must 20match 20configuration

+ [Get Started with Native Android Development - Hands-on Challenge - error=redirect_uri_mismatch&error_description=redirect_uri 20must 20match 20configuration](https://trailhead.salesforce.com/trailblazer-community/feed/0D54V00007PhTsJ)