Salesforce APIをJWTで保護する:統合エンジニア向けガイド

背景と応用シナリオ

Salesforce 統合エンジニアとして、私たちは日々、様々な外部システムと Salesforce を安全かつ効率的に接続するという課題に直面しています。特に、ユーザーの介在なしにシステム間で直接通信を行うサーバー間連携(Server-to-Server Integration)のシナリオでは、認証情報の管理が極めて重要になります。従来のユーザー名とパスワードを使用した認証フローは、セキュリティリスクが高く、Salesforce でも推奨されていません。そこで登場するのが、OAuth 2.0 JWT Bearer Flow です。

このフローは、JSON Web Token (JWT) を利用して、事前にシステム間の信頼関係を確立し、ユーザーのパスワードを直接扱うことなく Salesforce API へのアクセス権(アクセストークン)を取得する仕組みです。例えば、以下のようなシナリオで絶大な効果を発揮します。

  • 夜間バッチ処理: 外部のデータウェアハウスから毎晩 Salesforce にデータを同期する。
  • イベント駆動型連携: 外部の ERP システムで注文が作成されたら、リアルタイムで Salesforce の商談を更新する。
  • 継続的インテグレーション/継続的デプロイメント(CI/CD): デプロイメントパイプラインが自動で Salesforce 環境にメタデータをデプロイする。

これらのシナリオでは、ユーザーが画面を操作することはありません。システムが自律的に動作する必要があります。JWT Bearer Flow は、このような非対話型の連携において、最高のセキュリティと信頼性を提供する標準的な認証方式と言えるでしょう。この記事では、統合エンジニアの視点から、JWT の基本原理、Salesforce での実装方法、そして運用上の注意点について詳しく解説していきます。


原理説明

JWT (JSON Web Token) は、認証情報を安全にやり取りするためのオープンスタンダード(RFC 7519)です。その実体は、署名された JSON オブジェクトであり、コンパクトで自己完結型(Self-contained)という特徴があります。JWT は、以下の3つの部分から構成され、それぞれがドット(.)で区切られています。

Header . Payload . Signature

1. ヘッダー (Header)

ヘッダーは、トークンの種類(JWT)と、署名に使用されるアルゴリズム(例:RS256)を定義する JSON オブジェクトです。Salesforce の JWT Bearer Flow では、RS256(RSA 署名と SHA-256)の使用が必須です。

{
  "alg": "RS256",
  "typ": "JWT"
}

この JSON は Base64Url エンコードされ、JWT の最初の部分を形成します。

2. ペイロード (Payload)

ペイロードには、クレーム (Claims) と呼ばれる情報が含まれます。クレームは、エンティティ(通常はユーザー)と追加のメタデータに関する記述です。Salesforce がアクセストークンを発行するために、以下のクレームが必須となります。

  • iss (Issuer): 発行者。Salesforce の接続アプリケーション (Connected App) のコンシューマキー(Consumer Key)を指定します。
  • sub (Subject): 対象者。連携を実行する Salesforce ユーザーのユーザー名を指定します。このユーザーとして API が実行されます。
  • aud (Audience): 受信者。トークンを発行する先の URL を指定します。本番環境では https://login.salesforce.com、Sandbox 環境では https://test.salesforce.com となります。
  • exp (Expiration Time): 有効期限。トークンが無効になる時刻を UNIX タイムスタンプで指定します。セキュリティのため、非常に短い期間(例:3分以内)に設定することが強く推奨されます。

ペイロードの例:

{
  "iss": "3MVG9..._shortened_consumer_key...Qj",
  "sub": "integration.user@example.com",
  "aud": "https://login.salesforce.com",
  "exp": "1678886400"
}

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

3. 署名 (Signature)

署名は、JWT が改ざんされていないこと、そして信頼できる発行者によって作成されたことを証明するための最も重要な部分です。署名は以下の手順で生成されます。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey)

具体的には、エンコードされたヘッダーとペイロードをドットで連結した文字列を、接続アプリケーションの作成時に使用した X.509 証明書の秘密鍵 (Private Key) を使って署名します。Salesforce 側は、接続アプリケーションにアップロードされた公開鍵 (Public Key) を使ってこの署名を検証します。署名が正しければ、Salesforce は JWT を信頼し、ペイロード内の `sub` で指定されたユーザーのアクセストークンを発行します。これにより、パスワードをネットワーク上で送信することなく、安全な認証が実現されるのです。


示例代码

Salesforce 内で、別の Salesforce 組織や外部 API に対して JWT を生成し、認証を行うシナリオを考えてみましょう。Apex を使用して JWT を生成し、トークンを要求するコード例を以下に示します。このコードは Salesforce の公式ドキュメントに基づいています。

Apex で JWT を生成する

まず、`Auth.JWT` クラスと `Auth.JWS` クラスを使用して JWT を構築し、署名します。この例では、証明書と鍵が Salesforce の鍵管理機能で管理されていることを前提としています。

// JWT の発行者 (iss) として、接続アプリケーションのコンシューマキーを設定
String consumerKey = 'YOUR_CONNECTED_APP_CONSUMER_KEY';

// トークンの受信者 (aud) を設定。本番なら login.salesforce.com
String authUrl = 'https://login.salesforce.com';

// 認証するユーザー名 (sub) を設定
String subject = 'integration.user@example.com';

// JWT の有効期限を設定 (UNIX エポック秒)
// 現在時刻から2分後に設定
Long expiration = System.currentTimeMillis() / 1000 + 120;

// 1. JWT のクレームセットを JSON 文字列として構築
String claims = '{"iss":"' + consumerKey + '",'
            + '"sub":"' + subject + '",'
            + '"aud":"' + authUrl + '",'
            + '"exp":"' + expiration + '"}';

// 2. Auth.JWT オブジェクトを初期化
Auth.JWT jwt = new Auth.JWT();
jwt.setIss(consumerKey);
jwt.setSub(subject);
jwt.setAud(authUrl);
// 追加のカスタムクレームも設定可能
// jwt.setAdditionalClaims(new Map{'scope' => 'api'});

// 3. JWS (JSON Web Signature) を生成
// 最後の引数には、[設定] > [セキュリティ] > [証明書と鍵の管理] で設定した証明書の一意の名前を指定します。
// これにより、Salesforce が安全に管理している秘密鍵で署名が行われます。
Auth.JWS jws = new Auth.JWS(jwt, 'Your_Certificate_Unique_Name');

// 4. コンパクトなシリアル化形式で JWT 文字列を取得
String token = jws.getCompactSerialization();

System.debug('Generated JWT: ' + token);

生成した JWT を使ってアクセストークンを要求する

次に、生成した JWT を使って Salesforce のトークンエンドポイントに POST リクエストを送信し、アクセストークンを取得します。

// 前のステップで生成した JWT トークン
String jwtToken = token; 

// トークンエンドポイントの URL
String tokenEndpoint = 'https://login.salesforce.com/services/oauth2/token';

// リクエストボディを構築
// grant_type は JWT Bearer Flow を示す固定値
// assertion に JWT トークンをセット
String requestBody = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer'
                   + '&assertion=' + EncodingUtil.urlEncode(jwtToken, 'UTF-8');

// HTTP リクエストを準備
HttpRequest req = new HttpRequest();
req.setEndpoint(tokenEndpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setBody(requestBody);

// HTTP リクエストを送信し、レスポンスを取得
Http http = new Http();
HttpResponse res = http.send(req);

// レスポンスを処理
if (res.getStatusCode() == 200) {
    // 成功した場合、JSON をパースしてアクセストークンを取得
    Map tokenResponse = (Map) JSON.deserializeUntyped(res.getBody());
    String accessToken = (String) tokenResponse.get('access_token');
    String instanceUrl = (String) tokenResponse.get('instance_url');
    
    System.debug('Access Token: ' + accessToken);
    System.debug('Instance URL: ' + instanceUrl);
    
    // このアクセストークンを使って API を呼び出す
    // ...
} else {
    // エラー処理
    System.debug('Error Code: ' + res.getStatusCode());
    System.debug('Error Body: ' + res.getBody());
}

注意事項

JWT Bearer Flow を実装・運用する際には、以下の点に注意が必要です。

1. 接続アプリケーション (Connected App) の設定

  • OAuth の有効化: 接続アプリケーションで「OAuth 設定の有効化」を必ずチェックしてください。
  • デジタル署名の使用: 「デジタル署名を使用」をチェックし、事前に作成した X.509 証明書(公開鍵)をアップロードします。この証明書に対応する秘密鍵が JWT の署名に使われます。
  • OAuth スコープ: 連携に必要な最小限のスコープ(例:「API を介したユーザーデータの管理 (api)」)を選択してください。

2. ユーザーの事前承認

JWT Bearer Flow で最もハマりやすいポイントの一つが、ユーザーの事前承認です。`sub` クレームで指定されたユーザーは、その接続アプリケーションの使用を事前に許可している必要があります。管理者は、プロファイルまたは権限セット単位でアプリケーションへのアクセスを事前に承認できます。

[設定] > [アプリケーションを管理] > [接続アプリケーション] > [該当のアプリケーションを選択] > [ポリシーを編集] に移動し、「許可されているユーザー」ポリシーを「管理者が承認したユーザは事前承認済み」に変更します。その後、関連リストから対象のプロファイルまたは権限セットを追加します。

3. 証明書と秘密鍵の管理

JWT のセキュリティの根幹は秘密鍵の管理にあります。秘密鍵が漏洩すると、第三者がシステムになりすまして Salesforce にアクセスできてしまいます。

  • 安全な保管: 秘密鍵は、Azure Key Vault、AWS KMS、または Salesforce の Shield Platform Encryption Keystore のような安全な場所に保管してください。Apex から呼び出す場合は、前述のコード例のように Salesforce の証明書と鍵の管理機能を使うのが最も安全です。
  • 定期的なローテーション: 組織のセキュリティポリシーに従い、定期的に証明書をローテーション(更新)するプロセスを確立してください。

4. クロック同期 (Clock Skew)

JWT の `exp` クレームは、クライアントサーバーと Salesforce サーバー間の時刻のズレ(クロックスキュー)に非常に敏感です。Salesforce は最大3分間の時刻のズレを許容しますが、クライアント側のサーバー時刻が正確であることを保証するために、NTP (Network Time Protocol) を使用して時刻を同期させることが強く推奨されます。

5. エラーハンドリング

トークンリクエストが失敗した場合、Salesforce は詳細なエラーコードを返します。一般的なエラーとその原因を理解しておくことが重要です。

  • invalid_grant: ユーザーがアプリケーションを承認していない、ユーザーが無効になっている、証明書が一致しない、JWT の形式が不正など、最も一般的なエラーです。
  • invalid_client_id: `iss` クレームのコンシューマキーが正しくありません。
  • expired_token: JWT の `exp` クレームが過去の時刻になっています。クロックスキューが原因である可能性が高いです。

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

OAuth 2.0 JWT Bearer Flow は、Salesforce のサーバー間連携における認証のゴールドスタンダードです。パスワードを直接扱わず、デジタル署名に基づく信頼関係を構築することで、極めて高いセキュリティレベルを実現します。統合エンジニアとしてこのフローをマスターすることは、堅牢でスケーラブルな連携ソリューションを構築するための必須スキルです。

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

  1. 専用の連携ユーザーを使用する: 実際の人間が使用するユーザーアカウントではなく、API 連携専用のユーザーを作成し、最小権限の原則(Principle of Least Privilege)に従って必要な権限のみを付与します。
  2. 秘密鍵を厳重に管理する: Salesforce Platform の外で秘密鍵を扱う場合は、HSM (Hardware Security Module) やクラウドのキー管理サービスを利用します。Apex 内で完結する場合は、Salesforce の証明書と鍵の管理機能を利用します。
  3. JWT の有効期限を短くする: `exp` クレームは可能な限り短く(例:2〜3分)設定し、万が一トークンが漏洩した際の影響を最小限に抑えます。
  4. アクセストークンをキャッシュする: 一度取得したアクセストークンは、有効期限が切れるまで再利用します。API コールごとに新しいトークンを要求すると、不要なオーバーヘッドとガバナ制限の消費につながります。
  5. 堅牢な鍵ローテーション戦略を導入する: 定期的に証明書を更新し、古い証明書を無効化するプロセスを確立・自動化します。

これらの原則を守ることで、Salesforce を中心としたエンタープライズシステム連携を、安全かつ効率的に実現できるでしょう。

コメント