🤝

JWT認証によるSalesforce API連携手順について

2025/01/23に公開

本記事について

本記事は、Salesforce API連携を初めて試す方や、JWT認証を用いた接続方法を学びたい方向けに、JWT認証を用いたSalesforce API連携手順について解説します。

この記事で学べること

  • JWT認証を使用したSalesforce API連携の基本
  • KotlinスクリプトによるAPI接続の実装
  • Salesforceからデータを取得する具体的な手順

想定するユースケース

Salesforce APIを活用することで、以下のような課題を解決できます

  • Salesforceと社内システム間でデータを同期し、二重管理を防止
  • Salesforceとサードパーティアプリを連携し、業務効率化や自動化を実現

また本記事では以下のような構成で、ローカル環境のKotlinスクリプトからAPI接続してSalesforce上のデータを取得する、という処理を作ることを目指します。

1. 接続準備

API接続するためのローカル環境、Salesforceでの準備作業について解説します。

1.1. アカウント作成

まずは開発者用のアカウントを準備します。開発者用ユーザーの場合は以下のリンクから作成することができます。

https://developer.salesforce.com/signup

1.2. 秘密鍵・証明書の作成

ローカル環境とSalesforce間でAPI接続するための秘密鍵・証明書を作成します。

コンソールで以下を実行することで秘密鍵を発行します。

openssl genrsa -out server.key 2048

続いてコンソールで以下を実行することで証明書ファイルを発行します。

openssl req -new -x509 -key server.key -out server.crt -days 365

コマンド実行すると以下の入力を求められるので、それぞれ入力します。

  • Country Name (C): 国コード(例: JP
  • State or Province Name (ST): 都道府県(例: Tokyo
  • Locality Name (L): 市町村(例: Minato
  • Organization Name (O): 組織名(例: ExampleCompany
  • Organizational Unit Name (OU): 部署名(任意)
  • Common Name (CN): ユーザー名やドメイン名(例: example-dev
  • Email Address: 任意のメールアドレス

1.3. 外部クライアントアプリケーションを作成

Salesforceに外部クライアントアプリケーションを作成します。

OAuthポリシーは以下のように設定します。

  • プラグインポリシー
    • 許可されているユーザーを「管理者が承認したユーザーは事前承認済み」にする
    • 「すべてのユーザーは自己承認可能」だと、API接続時も承認手続きが必要になる
  • プロファイルを選択
    • 「選択済みプロファイル」に接続したいユーザーのプロファイルを追加
    • 1.1で作成した開発者用アカウントのプロファイルは「システム管理者」であるため、以下のスクリーンショットでは「システム管理者」を指定している

次にOAuth設定で「コンシューマー鍵」と「コンシューマーの秘密」を取得します。

以下画面で「コンシューマーと秘密」をクリックするとワンタイムコードがメール送信され、ワンタイムコードを入力すると「コンシューマー鍵」と「コンシューマーの秘密」を取得できます。

また、この画面の「OAuth範囲」で「選択したOAuth範囲」にapi, fullが含まれるようにします。

そして「フローの有効化」で「JWT ベアラーフローを有効化」を有効にします。

有効にすると「ファイルをアップロード」が表示されるので、ここから1.2で作成した証明書 server.crt をアップロードします。

ここまでで、Salesforceでの準備作業が完了しました。次に、Kotlinを使ってSalesforce APIに接続し、実際にデータを取得する手順を解説していきます。

2. 接続手順

2.1. ディレクトリ構成とビルド設定

このセクションでは、プロジェクトのディレクトリ構成とビルド設定を紹介します。

ディレクトリ構成

以下の構成で、Kotlinを用いてSalesforce APIとの連携を実装します。

  • repository
    • build.gradle.kts: 必要な依存関係とアプリケーション設定を定義。
    • main/kotlin
      • SalesforceJwt.kt: メインのKotlinスクリプト。JWTの生成、アクセストークンの取得、Salesforceデータの取得を実装。
    • main/resources
      • server.key: API接続で使用する秘密鍵ファイル。
      • server.crt: API接続で使用する証明書ファイル。

ビルド設定 build.gradle.kts

以下は、必要なライブラリを含むGradle設定ファイルです。

plugins {
    kotlin("jvm") version "1.9.0"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    // Apache HttpClient
    implementation("org.apache.httpcomponents.client5:httpclient5:5.1")

    // JWT (JSON Web Token)
    implementation("io.jsonwebtoken:jjwt-api:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")

    // ロギング用
    implementation("org.slf4j:slf4j-simple:2.0.9")

    // JSON処理用 (Jackson)
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
}

application {
    mainClass.set("SalesforceJwtKt")
}
  • Apache HttpClient: HTTP通信を行うためのライブラリ。
  • JWT: Salesforce認証で使用するJWTの生成に必要。
  • Jackson: JSONデータの操作用。

2.2. JWTの作成と認証プロセスの概要

Salesforceと連携するためには、JWT認証を使用してアクセストークンを取得する必要があります。このセクションでは認証に必要なプロセスをSalesforceJwt.kt 中に実装する関数ごとに解説します。

2.2.1 秘密鍵の読み込み loadPrivateKey

秘密鍵を使用してJWTを生成するために、main/resources/server.keyからRSA秘密鍵を読み込みます。

fun loadPrivateKey(path: String): PrivateKey {
    val keyBytes: ByteArray

    // リソースファイルから秘密鍵を読み込む
    val resourceAsStream: InputStream = object {}.javaClass.getResourceAsStream(path)
        ?: throw IllegalArgumentException("Resource not found: $path")

    resourceAsStream.use { inputStream ->
        val keyString = inputStream.bufferedReader().use { it.readText() }
        // 秘密鍵文字列から不要な部分を削除しデコード
        val privateKeyPEM = keyString
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace("\\s".toRegex(), "")
        // Base64でデコードし、RSA鍵として読み込み。
        keyBytes = Base64.getDecoder().decode(privateKeyPEM)
    }

    val spec = PKCS8EncodedKeySpec(keyBytes)
    val keyFactory = KeyFactory.getInstance("RSA")
    return keyFactory.generatePrivate(spec)
}

2.2.2. JWTの生成 generateJWT

JWTを生成し、Salesforceの認証エンドポイントに送信します。

// Salesforce の認証に使用する基本 URL
val baseUrl = "https://login.salesforce.com"

// JWT を生成する
fun generateJWT(consumerKey: String, username: String, privateKey: PrivateKey): String {
    val now = Date()
    val exp = Date(now.time + 300 * 1000) // 有効期限を5分に設定

    return Jwts.builder()
        .setIssuer(consumerKey) // クライアントID(コンシューマー鍵)を設定
        .setSubject(username)  // ユーザー名を設定
        .setAudience(baseUrl)  // Salesforce の認証エンドポイント
        .setExpiration(exp)    // トークンの有効期限
        .signWith(privateKey, SignatureAlgorithm.RS256) // RSA-SHA256で署名
        .compact()
}

2.2.3. アクセストークンの取得 getAccessToken

生成したJWTを使い、Salesforce APIの認証エンドポイントからアクセストークンを取得します。

// アクセストークンレスポンス用のデータクラス
data class AccessTokenResponse(
    val id: String,
    val access_token: String,
    val scope: String,
    val instance_url: String,
    val token_type: String,
    val api_instance_url: String,
)

// JSONレスポンスをデータクラスに変換する関数
fun jsonToAccessTokenResponse(json: String): AccessTokenResponse {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(json)
}

// アクセストークンを取得する関数
fun getAccessToken(jwt: String): AccessTokenResponse {
    val client: CloseableHttpClient = HttpClients.createDefault()
    val url = "$baseUrl/services/oauth2/token"
    val post = HttpPost(url)

    // リクエストパラメータを設定
    val params = listOf(
        BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
        BasicNameValuePair("assertion", jwt)
    )
    post.entity = UrlEncodedFormEntity(params)

    client.execute(post).use { response ->
        return jsonToAccessTokenResponse(
            response.entity.content.bufferedReader().use { it.readText() }
        )
    }
}

2.3. Salesforceデータの取得 getData

アクセストークンを使用してSalesforce REST APIを呼び出ます。SOQLクエリを実行しSalesforceデータを取得します。

SOQLとはSalesforce Object Query Languageの略で、Salesforceに保存されたオブジェクトのデータを検索するためのSQLライクなクエリ言語です。

SOQLについてはSalesforce Object Query Language (SOQL)も参考にしてください。

ここではSalesforce上の標準オブジェクトであるAccount(取引先)のデータをSOQLで取得しています。

なおAPIは2025/01/15現在で最新のバージョンであるv62.0を利用しています。

// JSONを整形して表示する
fun formatJson(json: String): String {
    val mapper = jacksonObjectMapper()
    val jsonNode = mapper.readTree(json)
    return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode)
}

// Salesforce に SOQL クエリを実行する
fun getData(accessToken: String, instanceUrl: String) {
    val client: CloseableHttpClient = HttpClients.createDefault()

    // 実行する SOQL クエリ
    val soql = """
    SELECT
        Id,
        Name
    FROM Account
    LIMIT 5
    """.trimIndent()

    // クエリを URL エンコード
    val encodedSoql = URLEncoder.encode(soql, "UTF-8")
    val url = "$instanceUrl/services/data/v62.0/query?q=${encodedSoql.replace(" ", "+")}"

    val get = HttpGet(url)
    get.addHeader("Authorization", "Bearer $accessToken") // アクセストークンをヘッダーに設定
    get.addHeader("Content-Type", "application/json")

    client.execute(get).use { response ->
        val r = response.entity.content.bufferedReader().use { it.readText() }
        println(formatJson(r)) // 結果を整形して出力
    }
}

2.4. スクリプト全体の実行フロー

スクリプト全体をまとめると以下のようになります。

import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.apache.hc.client5.http.classic.methods.HttpPost
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity
import org.apache.hc.core5.http.message.BasicNameValuePair
import java.io.InputStream
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.*
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.apache.hc.client5.http.classic.methods.HttpGet
import java.net.URLEncoder

// Salesforce の認証に使用する基本 URL
val baseUrl = "https://login.salesforce.com"

// RSA秘密鍵をロードする関数
fun loadPrivateKey(path: String): PrivateKey {
    val keyBytes: ByteArray

    // リソースファイルから秘密鍵を読み込む
    val resourceAsStream: InputStream = object {}.javaClass.getResourceAsStream(path)
        ?: throw IllegalArgumentException("Resource not found: $path")

    resourceAsStream.use { inputStream ->
        val keyString = inputStream.bufferedReader().use { it.readText() }
        // 秘密鍵文字列から不要な部分を削除しデコード
        val privateKeyPEM = keyString
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replace("\\s".toRegex(), "")
        keyBytes = Base64.getDecoder().decode(privateKeyPEM)
    }

    val spec = PKCS8EncodedKeySpec(keyBytes)
    val keyFactory = KeyFactory.getInstance("RSA")
    return keyFactory.generatePrivate(spec)
}

// JWT を生成する関数
fun generateJWT(consumerKey: String, username: String, privateKey: PrivateKey): String {
    val now = Date()
    val exp = Date(now.time + 300 * 1000) // 有効期限を5分に設定

    return Jwts.builder()
        .setIssuer(consumerKey) // クライアントID(コンシューマー鍵)を設定
        .setSubject(username)  // ユーザー名を設定
        .setAudience(baseUrl)  // Salesforce の認証エンドポイント
        .setExpiration(exp)    // トークンの有効期限
        .signWith(privateKey, SignatureAlgorithm.RS256) // RSA-SHA256で署名
        .compact()
}

// アクセストークンレスポンス用のデータクラス
data class AccessTokenResponse(
    val id: String,
    val access_token: String,
    val scope: String,
    val instance_url: String,
    val token_type: String,
    val api_instance_url: String,
)

// JSONレスポンスをデータクラスに変換する関数
fun jsonToAccessTokenResponse(json: String): AccessTokenResponse {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(json)
}

// アクセストークンを取得する関数
fun getAccessToken(jwt: String): AccessTokenResponse {
    val client: CloseableHttpClient = HttpClients.createDefault()
    val url = "$baseUrl/services/oauth2/token"
    val post = HttpPost(url)

    // リクエストパラメータを設定
    val params = listOf(
        BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
        BasicNameValuePair("assertion", jwt)
    )
    post.entity = UrlEncodedFormEntity(params)

    client.execute(post).use { response ->
        return jsonToAccessTokenResponse(
            response.entity.content.bufferedReader().use { it.readText() }
        )
    }
}

// JSONを整形して表示する関数
fun formatJson(json: String): String {
    val mapper = jacksonObjectMapper()
    val jsonNode = mapper.readTree(json)
    return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode)
}

// Salesforce に SOQL クエリを実行する関数
fun getData(accessToken: String, instanceUrl: String) {
    val client: CloseableHttpClient = HttpClients.createDefault()

    val soql = """
    SELECT
        Id,
        Name
    FROM Account
    LIMIT 5
    """.trimIndent()

    // クエリを URL エンコード
    val encodedSoql = URLEncoder.encode(soql, "UTF-8")
    val url = "$instanceUrl/services/data/v62.0/query?q=${encodedSoql.replace(" ", "+")}"

    val get = HttpGet(url)
    get.addHeader("Authorization", "Bearer $accessToken") // アクセストークンをヘッダーに設定
    get.addHeader("Content-Type", "application/json")

    client.execute(get).use { response ->
        val r = response.entity.content.bufferedReader().use { it.readText() }
        println(formatJson(r)) // 結果を整形して出力
    }
}

fun main() {
    val consumerKey = "XXXX" // クライアントID, 1.3で作成したコンシューマーキーを指定する
    val username = "example@example.com" // ユーザー名
    val privateKeyPath = "/server.key" // 秘密鍵のパス

    // 秘密鍵をロード
    val privateKey = loadPrivateKey(privateKeyPath)

    // JWT を生成
    val jwt = generateJWT(consumerKey, username, privateKey)

    // アクセストークンを取得
    val accessTokenResponse = getAccessToken(jwt)

    // アクセストークンレスポンスをパース
    val accessToken = jsonToAccessTokenResponse(accessTokenResponse)

    // Salesforce にデータ取得リクエストを実行
    getData(accessToken.access_token, accessToken.instance_url)
}

このスクリプトを実行すれば、Salesforce APIから取引先(Account)オブジェクトのデータをJSON形式で取得できます。具体的には以下のような実行結果になります。

{
  "totalSize" : 5,
  "done" : true,
  "records" : [ {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v62.0/sobjects/Account/001F900001pJgsFIAS"
    },
    "Id" : "001F900001pJgsFIAS",
    "Name" : "Grand Hotels & Resorts Ltd"
  }, {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v62.0/sobjects/Account/001F900001pJgsHIAS"
    },
    "Id" : "001F900001pJgsHIAS",
    "Name" : "Express Logistics and Transport"
  }, {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v62.0/sobjects/Account/001F900001pJgsIIAS"
    },
    "Id" : "001F900001pJgsIIAS",
    "Name" : "University of Arizona"
  }, {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v62.0/sobjects/Account/001F900001pJgsGIAS"
    },
    "Id" : "001F900001pJgsGIAS",
    "Name" : "United Oil & Gas Corp."
  }, {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v62.0/sobjects/Account/001F900001pJgsMIAS"
    },
    "Id" : "001F900001pJgsMIAS",
    "Name" : "sForce"
  } ]
}

注意点

本記事は2025年1月時点のSalesforce APIのドキュメント、仕様に基づいた記事となっております。参考にされる際は最新のドキュメント、仕様を確認の上自己責任で参考にしてください。

権限について

権限セット・プロファイル単位の設定により、オブジェクト・項目単位で閲覧・更新権限を付与することができます。これにより柔軟な権限設定が可能ですが、場合によっては権限が不足して必要な情報が見れなかったり、不必要な情報が見れる権限にしてしまう、というリスクがあります。

リクエストの制限

プランごとに以下のようなリクエスト制限があります。

エディション 1日あたりのリクエスト制限
Enterprise Edition / Professional Edition 1ユーザーあたり1,000リクエスト
Unlimited Edition 1ユーザーあたり5,000リクエスト
Developer Edition 1日あたり15,000リクエスト

最新の情報は以下をご確認ください。

実装・利用方法によってはリクエストの制限を超えてしまい、利用できなくなってしまうリスクがあります。

おわりに

本記事ではJWT認証を用いたSalesforce API連携手順について解説しました。この後のステップとしては、Salesforce APIの他のエンドポイントやSOQLについて学習していけば様々なデータ連携にSalesforceを活用できるようになります。

参考リンク

サーバー間インテグレーション用の OAuth 2.0 JWT ベアラーフロー

株式会社ログラス テックブログ

Discussion