Salesforce JWTベアラーフローをマスターする:安全なサーバー間連携のための技術解説

背景と応用シーン

現代のエンタープライズシステムにおいて、システム間のデータ連携は不可欠です。Salesforceもまた、外部システムとの連携を強力にサポートしており、その認証方法として様々な OAuth 2.0 フローを提供しています。その中でも、特にサーバー間の連携(Server-to-Server Integration)において絶大な効果を発揮するのが JWT (JSON Web Token、JSONウェブトークン) Bearer Flow です。

例えば、以下のようなシナリオを考えてみましょう。

  • 夜間に外部のデータウェアハウスからSalesforceへ最新の販売データを同期するバッチ処理。
  • ユーザーの操作を介さず、ERPシステムで更新された在庫情報をリアルタイムでSalesforceの商品オブジェクトに反映させる仕組み。
  • CI/CDパイプラインの一部として、デプロイメントツールがSalesforce組織のメタデータを自動で更新するプロセス。

これらのシナリオに共通するのは、「ユーザーの直接的な操作(ログイン画面でのID/パスワード入力など)が存在しない」という点です。このような「ヘッドレス」な連携において、ユーザーの認証情報をサーバーに保存することなく、安全かつ自律的にAPIアクセス権を取得する仕組みが求められます。JWT Bearer Flowは、この要求に見事に応えるための、セキュアで標準化された認証フローです。

このフローを利用することで、クライアントアプリケーションは事前にSalesforceと交換したデジタル証明書を用いて自己署名したJWTを生成し、それを提示することでアクセストークンを取得できます。これにより、パスワードの漏洩リスクを排除し、証明書の管理によってアクセス制御を行う、より高度なセキュリティモデルを構築することが可能になります。


原理説明

JWT Bearer Flowの核心は、その名の通りJWTにあります。JWTは、JSON形式の情報をコンパクトかつ安全にやり取りするためのオープンスタンダード(RFC 7519)です。JWTは3つのパートから構成され、それぞれがドット(`.`)で区切られています。

1. Header (ヘッダー)

ヘッダーは、トークンのタイプ(通常は"JWT")と、使用されている署名アルゴリズム(例:`RS256`)をJSON形式で含みます。このJSONはBase64UrlエンコードされてJWTの最初の部分を形成します。

2. Payload (ペイロード)

ペイロードには、クレーム (Claims) と呼ばれる、エンティティ(通常はユーザー)や追加のメタデータに関する情報が含まれます。SalesforceのJWT Bearer Flowでは、以下のクレームが重要です。

  • iss (Issuer): 発行者。Connected App (接続アプリケーション) の Consumer Key (Client ID) を指定します。
  • sub (Subject): 対象者。APIアクセスを許可するSalesforceユーザーのユーザー名を指定します。
  • aud (Audience): 受信者。トークンを発行する認証サーバーのURLを指定します。本番環境では `https://login.salesforce.com`、Sandbox環境では `https://test.salesforce.com` となります。
  • exp (Expiration Time): 有効期限。トークンが無効になるUNIXタイムスタンプをUTCで指定します。セキュリティ上、通常は数分程度の短い時間に設定します。

ペイロードもBase64Urlエンコードされ、JWTの2番目の部分を形成します。

3. Signature (署名)

署名は、JWTの完全性と認証を保証するための最も重要な部分です。エンコードされたヘッダーとペイロード、そしてサーバー側のみが知るPrivate Key (秘密鍵) を使用して、ヘッダーで指定されたアルゴリズム(RS256)で署名が生成されます。Salesforce側は、事前にConnected AppにアップロードされたDigital Certificate (デジタル証明書) に含まれる公開鍵を使ってこの署名を検証します。署名が正しければ、そのJWTが改ざんされておらず、信頼できる発行者(秘密鍵の所有者)から送られてきたことを確認できます。

認証フローのステップ

  1. 事前準備:
    • クライアントは、秘密鍵と公開鍵を含むX.509デジタル証明書を生成します。
    • SalesforceでConnected Appを作成し、「API (Enable OAuth Settings)」を有効にします。
    • 「Use digital signatures」にチェックを入れ、生成したデジタル証明書(公開鍵)をアップロードします。
    • 連携に使用するユーザーのプロファイルまたは権限セットに対して、このConnected Appへのアクセスを許可します(ポリシー設定)。
  2. JWTの生成: クライアントアプリケーションは、上記のクレームを含むJWTを生成し、自身の秘密鍵で署名します。
  3. トークンリクエスト: クライアントは、Salesforceのトークンエンドポイント (`/services/oauth2/token`) に対して、生成したJWTを `assertion` パラメータに含めてPOSTリクエストを送信します。
  4. 署名検証とトークン発行: Salesforceはリクエストを受け取ると、`iss` クレームからConnected Appを特定し、登録されているデジタル証明書を使ってJWTの署名を検証します。検証が成功し、かつ `sub` で指定されたユーザーがアプリへのアクセスを許可されている場合、Salesforceはアクセストークンを生成してクライアントに返却します。
  5. APIアクセス: クライアントは、取得したアクセストークンをAuthorizationヘッダーに含めて、Salesforceの各種API(REST API, SOAP APIなど)を呼び出します。

示例代码

ここでは、Apexを使用してSalesforce自身に対してJWT Bearer Flow認証を行う例を示します。このコードは、例えばあるSalesforce組織から別の組織へAPIコールを行う際などに利用できます。外部のJavaやPythonアプリケーションで実装する場合も、JWTの構築とHTTPリクエストの構造は同じです。

前提:

  • `my_named_credential` という名前付き資格情報に、トークンエンドポイント (`https://login.salesforce.com`) が設定されていること。
  • `My_Cert` という名前の証明書がSalesforceの証明書と鍵の管理で作成されていること。
  • Connected Appが設定済みであること。

ApexでのJWT生成とトークン取得

// JWT Bearer Flow を使用してアクセストークンを取得するメソッド
public class JWTBearerFlowExample {
    
    // 接続アプリケーションのConsumer Key
    private static final String CONSUMER_KEY = 'YOUR_CONNECTED_APP_CONSUMER_KEY';
    // 認証するユーザーのユーザー名
    private static final String USERNAME = 'user@example.com';
    // 認証サーバーのURL (本番環境)
    private static final String AUDIENCE = 'https://login.salesforce.com';

    public static String getAccessToken() {
        
        // 1. JWTの構築
        Auth.JWT jwt = new Auth.JWT();
        // iss (Issuer): Consumer Keyを設定
        jwt.setIss(CONSUMER_KEY);
        // sub (Subject): ユーザー名を設定
        jwt.setSub(USERNAME);
        // aud (Audience): 認証サーバーURLを設定
        jwt.setAud(AUDIENCE);
        
        // 追加のクレームとして、有効期限を設定(ここでは5分後)
        // UNIXタイムスタンプは秒単位であるため、ミリ秒から変換
        Long rightNow = Datetime.now().getTime() / 1000;
        Long expirationTime = rightNow + 300; // 5 minutes * 60 seconds
        Map<String, Object> claims = new Map<String, Object>();
        claims.put('exp', expirationTime);
        jwt.setAdditionalClaims(claims);

        // 2. JWS (JSON Web Signature) の生成
        // 'My_Cert' はSalesforceに登録された証明書の名前
        Auth.JWS jws = new Auth.JWS(jwt, 'My_Cert');
        
        // Base64-encodedのJWT文字列を取得
        String token = jws.getCompactSerialization();

        // 3. トークンエンドポイントへのリクエスト作成
        // 名前付き資格情報を使用してエンドポイントを指定
        String endpoint = 'callout:my_named_credential/services/oauth2/token';
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('POST');
        
        // リクエストボディをURLエンコード形式で構築
        String body = 'grant_type=' + EncodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:jwt-bearer', 'UTF-8')
                    + '&assertion=' + EncodingUtil.urlEncode(token, 'UTF-8');
        req.setBody(body);
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');

        // 4. リクエストの送信とレスポンスの処理
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        String accessToken = null;
        if (res.getStatusCode() == 200) {
            Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            accessToken = (String) responseBody.get('access_token');
            System.debug('Access Token successfully obtained: ' + accessToken);
        } else {
            System.debug('Error occurred: ' + res.getStatusCode() + ' ' + res.getBody());
            // エラー処理をここに追加
        }
        
        return accessToken;
    }
}

注意事項

JWT Bearer Flowを実装・運用する際には、いくつかの重要な注意点があります。

権限と設定

  • Connected Appのユーザーポリシー: Connected Appの設定で、「Permitted Users」ポリシーを「Admin approved users are pre-authorized」に設定することが強く推奨されます。これにより、管理者はプロファイルまたは権限セット単位で、このアプリを使用できるユーザーを明示的に許可できます。許可されていないユーザーで認証しようとすると、`user_not_approved` エラーが発生します。
  • 証明書と秘密鍵の管理: 秘密鍵は絶対に漏洩させてはなりません。クライアントアプリケーションのサーバー上で、厳格なアクセス制御のもとで安全に保管してください。鍵が漏洩した場合は、ただちに新しい証明書と鍵ペアを生成し、Connected Appの証明書を更新する必要があります。
  • APIアクセスのスコープ: Connected Appで選択したOAuthスコープ(例: `api`, `full`, `refresh_token`)が、取得したアクセストークンで実行できる操作を決定します。連携に必要な最小限のスコープを割り当てるようにしてください。

API制限とエラー処理

  • 時刻の同期: JWTの `exp` (有効期限) クレームは、Salesforceサーバーの時刻と比較されます。クライアントサーバーの時刻が大幅にずれている(クロックスキュー)と、トークンが有効期限切れ、または未来のトークンと判断され、認証が失敗する可能性があります。NTP (Network Time Protocol) などを使用して、サーバーの時刻を正確に同期させてください。
  • 一般的なエラーコード:
    • invalid_grant: 最もよく発生するエラーの一つ。JWTの署名が無効、クレーム(iss, sub, audなど)が正しくない、ユーザーがアプリを承認していない、ユーザーが無効になっている、などが原因です。
    • invalid_client_id: `iss` クレームのConsumer Keyが正しくありません。
    • expired_assertion: JWTの有効期限 (`exp`) が切れています。
    • invalid_scope: リクエストで指定されたスコープがConnected Appで許可されていません。
  • APIコール制限: アクセストークンを取得した後のAPIコールは、通常のSalesforce APIガバナ制限に従います。大量のデータを扱うバッチ処理などの場合は、Bulk APIの使用も検討してください。

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

Salesforce JWT Bearer Flowは、サーバー間連携における認証のゴールドスタンダードと言えるでしょう。ユーザーのパスワードを扱う必要がなく、デジタル署名に基づく堅牢なセキュリティを提供するため、自動化されたプロセスやバックエンドシステムとの連携に最適です。

最後に、JWT Bearer Flowを実装する上でのベストプラクティスをまとめます。

  1. 専用の連携ユーザーを作成する: 連携のためだけに、最小権限の原則に従った専用のAPIユーザーを作成します。これにより、万が一認証情報が漏洩した際の影響範囲を限定できます。
  2. 秘密鍵を安全に保管する: ハードウェアセキュリティモジュール(HSM)や、クラウドプロバイダーが提供するシークレット管理サービス(AWS Secrets Manager, Azure Key Vaultなど)を利用して、秘密鍵を厳重に管理します。
  3. JWTの有効期限を短く設定する: `exp` クレームで設定する有効期限は、リクエストがネットワークを通過するのに十分な、かつできるだけ短い時間(例:2〜5分)に設定します。これにより、万が一JWTが傍受されたとしても、再利用されるリスクを最小限に抑えられます。
  4. Connected Appの利用状況を監視する: SalesforceのイベントモニタリングやConnected App利用状況レポートを活用し、不審なアクティビティがないか定期的に監視します。
  5. 適切なエラーハンドリングとリトライ処理を実装する: ネットワークの問題や一時的なサーバーエラーに備え、クライアント側で適切なリトライロジック(指数バックオフなど)を実装しておくことが重要です。

これらの原則を守ることで、Salesforceとの連携をセキュアかつ安定的に運用し、ビジネス価値を最大化することができます。

コメント