Salesforce JWTベアラーフロー徹底解説:安全なサーバー間連携のために

背景と適用シナリオ

皆さん、こんにちは。Salesforce 統合エンジニアです。日々の業務で、Salesforce と外部システムを連携させることは非常に一般的です。例えば、基幹システム(ERP)から夜間に売上データを同期したり、外部のデータウェアハウスに Salesforce のデータをエクスポートしたり、CI/CD パイプラインからメタデータをデプロイしたりと、そのユースケースは多岐にわたります。

これらの連携シナリオの多くに共通する要件は「ユーザーの介在なしに、バックエンドで自動的に処理を実行したい」という点です。従来の OAuth 2.0 フロー、例えば Authorization Code Flow などは、ユーザーがブラウザで Salesforce のログイン画面にリダイレクトされ、ID とパスワードを入力して認証・認可を行うプロセスを前提としています。しかし、サーバー間の自動連携において、この「手動ログイン」のステップは実行不可能です。

この課題を解決するために設計されたのが、OAuth 2.0 JWT Bearer Flow です。JWT は JSON Web Token の略で、安全に情報をやり取りするためのオープンスタンダード(RFC 7519)です。このフローを利用することで、外部アプリケーションはユーザーのパスワードを直接扱うことなく、事前に設定したデジタル署名を用いて Salesforce の認証をパスし、API アクセスのためのアクセストークンを取得できます。これにより、ユーザーの介入を必要としない、セキュアで自動化されたサーバー間連携が実現可能になります。

具体的な適用シナリオとしては、以下のようなものが挙げられます。

  • 夜間バッチ処理:外部のスケジューラから起動されたジョブが、Salesforce API を呼び出してデータを一括更新する。
  • ETL/ELT ツールとの連携:MuleSoft、Informatica、Talend などのデータ統合ツールが Salesforce からデータを抽出し、データウェアハウスにロードする。
  • DevOps/CI/CD:Jenkins や GitHub Actions といったツールが、Apex テストの実行やメタデータのデプロイを自動的に行う。
  • IoT プラットフォーム連携:デバイスから送られてくるデータを、中継サーバーが Salesforce Platform Events に送信する。

本記事では、この強力な JWT Bearer Flow の仕組みから具体的な設定、実装、そして運用上の注意点までを、統合エンジニアの視点から詳しく解説していきます。


原理の説明

JWT Bearer Flow の核心は、クライアント(外部アプリケーション)が自己署名した JWT を Salesforce に提示し、それを信頼できる身分証明書として受け入れてもらう点にあります。この信頼関係は、非対称鍵暗号方式によって成り立っています。全体の流れを理解するために、まずは JWT そのものの構造と、フローのステップを分解して見ていきましょう。

JWT の構造

JWT は、ピリオド(.)で区切られた3つの Base64Url エンコードされた文字列から構成されます。

[Header].[Payload].[Signature]

  1. Header(ヘッダー):トークンのタイプ(JWT)と、署名に使用されるアルゴリズム(例:`RS256`)についての情報を含みます。
    {
      "alg": "RS256",
      "typ": "JWT"
    }
    
  2. Payload(ペイロード):Claim(クレーム)と呼ばれる、エンティティ(通常はユーザー)や追加のメタデータに関する情報を含みます。JWT Bearer Flow で Salesforce が要求する主要なクレームは以下の通りです。
    • iss (Issuer): トークンの発行者。Salesforce の接続アプリケーション (Connected App) のコンシューマキー(Client ID)を指定します。
    • sub (Subject): 連携を実行する Salesforce ユーザーのユーザー名。このユーザーとして API が実行されます。
    • aud (Audience): トークンの対象者。本番環境では `https://login.salesforce.com`、Sandbox 環境では `https://test.salesforce.com` を指定します。
    • exp (Expiration Time): トークンの有効期限。UNIX タイムスタンプ(1970年1月1日からの秒数)で指定します。セキュリティのため、通常は数分程度の短い期間に設定します。
  3. Signature(署名):ヘッダーとペイロードをエンコードした文字列を、指定されたアルゴリズムで署名したものです。クライアントは秘密鍵 (Private Key) で署名し、Salesforce はクライアントが事前に登録した公開鍵 (Public Key)(通常はデジタル証明書の形式)でこの署名を検証します。これにより、トークンが改ざんされておらず、正当な発行者からのものであることを確認できます。

認証フローのステップ

JWT Bearer Flow は、以下のステップで進行します。

  1. 事前準備:
    • クライアント側で、秘密鍵と自己署名デジタル証明書(公開鍵を含む)のペアを生成します。(例:`openssl` コマンドを使用)
    • Salesforce 側で接続アプリケーション (Connected App) を作成します。この際、「API (Enable OAuth Settings)」を有効にし、「Use digital signatures」にチェックを入れ、生成したデジタル証明書(公開鍵)をアップロードします。
    • 接続アプリケーションのコンシューマキー(Client ID)を控えておきます。
  2. JWT の作成と署名:

    クライアントアプリケーションは、必要なクレーム(`iss`, `sub`, `aud`, `exp`)を含む JWT をプログラムで作成し、保持している秘密鍵で署名します。

  3. アクセストークンの要求:

    クライアントは、作成した JWT を含めて、Salesforce のトークンエンドポイント(例:`/services/oauth2/token`)に POST リクエストを送信します。リクエストのボディには、`grant_type` として `urn:ietf:params:oauth:grant-type:jwt-bearer` を、`assertion` として生成した JWT を指定します。

  4. Salesforce による検証と応答:

    リクエストを受け取った Salesforce は、以下の検証を行います。

    • `iss` クレームのコンシューマキーに対応する接続アプリケーションが存在するか確認します。
    • 接続アプリケーションに登録されているデジタル証明書(公開鍵)を使って、JWT の署名を検証します。
    • `aud`, `exp` などのクレームが有効か(対象者が正しいか、有効期限が切れていないか)を確認します。
    • `sub` クレームで指定されたユーザーが、この接続アプリケーションの利用を許可しているか確認します。

    すべての検証が成功すると、Salesforce は API アクセスに必要なアクセストークン (Access Token) とインスタンス URL などを JSON 形式で返却します。

  5. API の呼び出し:

    クライアントは、取得したアクセストークンを HTTP の `Authorization: Bearer [アクセストークン]` ヘッダーに設定し、Salesforce の各種 API(REST API, SOAP API, Bulk API など)を呼び出します。


サンプルコード

ここでは、外部の Java アプリケーションが JWT を生成し、Salesforce にアクセストークンを要求する例を Salesforce の公式ドキュメントに基づいて紹介します。このコードは、JWT の作成と署名のプロセスを具体的に示しています。

注:このサンプルを実行するには、`java.security` や `java.util.Base64` などの標準 Java ライブラリに加え、JSON を扱うためのライブラリ(例:`org.json`)が必要です。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.util.Base64;
import org.json.JSONObject;

public class SalesforceJWTGenerator {

    public static String createJwt(String clientId, String username, String audience, String keyStorePath, String keyStorePassword, String keyAlias)
            throws KeyStoreException, NoSuchAlgorithmException, CertificateException,
                   FileNotFoundException, IOException, UnrecoverableKeyException {

        // 1. ヘッダーの作成
        // 署名アルゴリズムとして RS256 を指定します。
        JSONObject header = new JSONObject();
        header.put("alg", "RS256");

        // 2. ペイロード(クレームセット)の作成
        JSONObject claims = new JSONObject();
        claims.put("iss", clientId); // 発行者:接続アプリケーションのコンシューマキー
        claims.put("sub", username); // 対象ユーザー:連携を実行するユーザーのユーザー名
        claims.put("aud", audience); // 対象者:'https://login.salesforce.com' または 'https://test.salesforce.com'
        
        // 有効期限を現在時刻から3分後に設定
        long exp = Instant.now().getEpochSecond() + (3 * 60);
        claims.put("exp", exp);

        // 3. ヘッダーとペイロードを Base64Url エンコード
        String base64UrlEncodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.toString().getBytes("UTF-8"));
        String base64UrlEncodedClaims = Base64.getUrlEncoder().withoutPadding().encodeToString(claims.toString().getBytes("UTF-8"));
        String signingInput = base64UrlEncodedHeader + "." + base64UrlEncodedClaims;

        // 4. JKS (Java KeyStore) ファイルから秘密鍵を読み込む
        // 事前に 'keytool' などで秘密鍵を JKS ファイルに格納しておきます。
        KeyStore ks = KeyStore.getInstance("JKS");
        try (InputStream is = new FileInputStream(keyStorePath)) {
            ks.load(is, keyStorePassword.toCharArray());
        }
        PrivateKey privateKey = (PrivateKey) ks.getKey(keyAlias, keyStorePassword.toCharArray());

        // 5. 秘密鍵で署名を生成
        java.security.Signature signature = java.security.Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(signingInput.getBytes("UTF-8"));
        byte[] signedBytes = signature.sign();

        // 6. 署名を Base64Url エンコード
        String base64UrlEncodedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signedBytes);

        // 7. 完全な JWT を組み立てる
        return signingInput + "." + base64UrlEncodedSignature;
    }

    public static void main(String[] args) {
        try {
            // パラメータを環境に合わせて設定
            String clientId = "YOUR_CONNECTED_APP_CLIENT_ID";
            String username = "integration.user@example.com";
            String audience = "https://login.salesforce.com"; // Sandbox の場合は "https://test.salesforce.com"
            String keyStorePath = "/path/to/your/keystore.jks";
            String keyStorePassword = "keystorepassword";
            String keyAlias = "yourkeyalias";

            String jwt = createJwt(clientId, username, audience, keyStorePath, keyStorePassword, keyAlias);
            System.out.println("Generated JWT:");
            System.out.println(jwt);

            // この後、生成した jwt を使って Salesforce のトークンエンドポイントにリクエストを送信します。
            // curl -X POST https://login.salesforce.com/services/oauth2/token -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=[Generated JWT]"

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意事項

JWT Bearer Flow は非常に強力ですが、設定や運用を誤るとセキュリティリスクや連携エラーにつながる可能性があります。以下の点に十分注意してください。

接続アプリケーションと事前承認 (Pre-authorization)

JWT Bearer Flow で最もよくあるエラーの一つが `{"error":"invalid_grant","error_description":"user hasn't approved this consumer"}` です。これは、`sub` クレームで指定されたユーザーが、この接続アプリケーションの使用を許可していないことを意味します。

サーバー間連携ではユーザーが手動で許可画面をクリックできないため、管理者が事前に承認しておく必要があります。設定方法は以下の通りです。

  1. [設定] > [アプリケーションを管理] > [接続アプリケーション] から該当のアプリケーションを選択します。
  2. [編集] をクリックし、「OAuth ポリシー」セクションの「許可されているユーザー」「管理者が承認したユーザーは事前承認済み」に変更して保存します。
  3. アプリケーションの詳細画面に戻り、[プロファイル] または [権限セット] 関連リストから、このアプリケーションの使用を許可するプロファイルまたは権限セットを追加します。

連携専用のプロファイルや権限セットを作成し、それを `sub` に指定する連携用ユーザーに割り当てるのがベストプラクティスです。

秘密鍵の厳重な管理

JWT の署名に使用する秘密鍵は、システムの認証情報における最重要機密情報です。これが漏洩すると、攻撃者が正規のアプリケーションになりすまして Salesforce にアクセスできてしまいます。秘密鍵は、AWS Key Management Service (KMS), Azure Key Vault, HashiCorp Vault などのセキュアなキーストアサービスに保管し、アプリケーションからは実行時にのみセキュアに取得するように設計してください。ファイルシステムに平文で置いたり、ソースコードにハードコーディングしたりすることは絶対に避けてください。

クロックスキュー(サーバー間の時刻のズレ)

JWT の `exp`(有効期限)クレームは、サーバー間の時刻のズレ(クロックスキュー)に非常に敏感です。クライアントサーバーの時計が Salesforce のサーバーより数分進んでいると、生成された JWT は Salesforce にとって「未来のトークン」となり、逆に遅れていると「既に期限切れのトークン」と見なされ、認証が失敗する可能性があります。クライアントサーバーの時刻が NTP (Network Time Protocol) などで正確に同期されていることを必ず確認してください。

API 制限とエラーハンドリング

JWT Bearer Flow を介して取得したアクセストークンを使った API コールも、通常の API 制限(24時間あたりのコール数など)の対象となります。大量のデータを扱う連携を設計する際は、API ガバナ制限を考慮し、Bulk API の使用や適切なエラーハンドリング(リトライ処理など)を実装することが重要です。また、アクセストークンの取得に失敗した場合のログ出力や通知の仕組みも必ず組み込んでください。


まとめとベストプラクティス

OAuth 2.0 JWT Bearer Flow は、Salesforce とのサーバー間連携をセキュアかつ自動的に実現するための標準的な手法です。ユーザーのパスワードをシステム間で受け渡す必要がなく、デジタル署名に基づく堅牢な認証メカニズムを提供します。

最後に、統合エンジニアとして推奨するベストプラクティスをまとめます。

  • 専用の連携ユーザーを作成する:連携処理のためだけに、最小権限の原則に従って権限を絞った専用の Salesforce ユーザー(Integration User)を用意します。これにより、万が一認証情報が漏洩した際の影響範囲を最小限に抑えることができます。
  • 証明書のライフサイクル管理を計画する:デジタル証明書には有効期限があります。証明書の有効期限が切れると、すべての連携が停止してしまいます。証明書の有効期限を監視し、期限が切れる前に新しい証明書に交換するプロセス(キーローテーション)を計画し、自動化しておくことが重要です。
  • JWT の有効期限を短く設定する:`exp` クレームで指定する JWT の有効期限は、アクセストークンを取得するために必要な最小限の時間(通常は3〜5分)に設定します。これにより、万が一 JWT が漏洩した場合でも、それが悪用される時間を短くすることができます。
  • 詳細なログを記録する:いつ、どのシステムが、どのユーザーとしてアクセストークンを要求したか、そしてその結果(成功/失敗)を詳細にログとして記録します。これは、セキュリティ監査や障害発生時のトラブルシューティングに不可欠です。
  • 環境ごとに接続アプリケーションと証明書を分離する:開発、ステージング、本番といった各環境で、それぞれ異なる接続アプリケーションと鍵ペアを使用します。これにより、環境間のセキュリティ分離が保たれ、開発中の設定ミスが本番環境に影響を与えることを防ぎます。

これらのプラクティスを遵守することで、皆さんの Salesforce 連携はより安全で、堅牢で、管理しやすいものになるでしょう。是非、次のインテグレーションプロジェクトで JWT Bearer Flow を活用してみてください。

コメント