Salesforce OAuth 2.0 Authentication Flows for API Integration

Type: ERP Integration System: Salesforce (API v66.0, Spring '26) Confidence: 0.93 Sources: 8 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

System Profile

This card covers all OAuth 2.0 authentication flows supported by Salesforce for API integration as of Spring '26 (API v66.0). It covers the setup, token lifecycle, security considerations, and production gotchas for each flow. This is an authentication-focused card -- it does not cover what you can do after authentication (see related API capability cards for that). All editions that support the REST API support OAuth 2.0, but Connected App/External Client App creation requires admin permissions. [src1, src4]

PropertyValue
VendorSalesforce
SystemSalesforce Platform (Spring '26)
API SurfaceOAuth 2.0 Authentication
Current API Versionv66.0 (Spring '26)
Editions CoveredEnterprise, Unlimited, Developer, Performance
DeploymentCloud
API DocsOAuth Authorization Flows
StatusGA (all supported flows)

API Surfaces & Capabilities

Salesforce supports six active OAuth 2.0 flows (plus one removed). The choice depends on whether you need user context, can manage certificates, and whether a browser is available. [src1, src6]

OAuth FlowProtocolBest ForUser Context?Refresh Token?Certificate Required?Security Level
JWT BearerOAuth 2.0Server-to-server automation (ETL, middleware)No (integration user)No (new JWT per request)Yes (X.509)Highest
Client CredentialsOAuth 2.0Server-to-server (simpler setup)No (execution user)No (request new token)NoHigh
Web Server (Auth Code)OAuth 2.0User-context web applicationsYesYesNoHigh
PKCE (Auth Code + PKCE)OAuth 2.0Mobile apps, SPAs, public clientsYesYesNoHigh
User-Agent (Implicit)OAuth 2.0Legacy SPAs (not recommended)YesNoNoLow
Username-PasswordOAuth 2.0Testing only (breaks with MFA)Yes (specific user)NoNoVery Low
Device FlowOAuth 2.0Removed September 2025N/AN/AN/ARemoved

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
JWT assertion validity window5 minutes maxJWT Bearer flowexp claim must be <= 5 min from current time [src2]
Token endpoint rate limitShared with org API limitsAll OAuth flowsToken requests count against 24h API call limit [src1]
Max concurrent sessions per user5 (default)All flowsConfigurable per profile; oldest session killed when exceeded [src7]
Access token size~1.5 KBAll flowsOpaque token, not a JWT -- do not parse [src1]

Rolling / Daily Limits

Limit TypeValueWindowEdition Differences
API calls (includes token requests)100,000 base24h rollingEnterprise: 100K + (user count x 1,000); Unlimited: 5M; Developer: 15K [src1]
Token refresh requestsCounts against API limitPer requestEach refresh_token exchange = 1 API call [src7]
Connected App consent promptsNo hard limitPer userExcessive prompts may indicate misconfiguration [src1]

Token Lifetime Policies

Token TypeDefault LifetimeConfigurable?Policy Options
Access token2 hours (7,200s)Yes (session timeout)Tied to Connected App session policy [src7]
Refresh tokenUntil revoked (default)Yes (4 policy types)Until revoked / Expire after N / Expire if unused for N / Immediate expire [src7]
JWT assertion5 min maxNo (hard limit)Must be < 5 min from iat to exp [src2]

Authentication

Flow Selection Matrix

FlowUse WhenToken LifetimeRefresh?Notes
JWT BearerServer-to-server, no user interaction, certificate management availableSession timeout (default 2h)No -- issue new JWT each timeMost secure; recommended for production integrations [src2]
Client CredentialsServer-to-server, simpler setup, no certificate infrastructureSession timeout (default 2h)No -- request new tokenRequires designated execution user in Connected App [src3]
Web Server (Auth Code)User-context operations, interactive web appsAccess: 2h, Refresh: until revokedYesRequires callback URL; most common for web apps [src1]
PKCEMobile apps, SPAs, public clients (cannot store secret)Access: 2h, Refresh: until revokedYesReplaces User-Agent flow; code_verifier prevents interception [src6]
Username-PasswordTesting and development ONLYSession timeoutNoIncompatible with MFA; never use in production [src1, src6]

JWT Bearer Flow -- Detailed Setup

The JWT Bearer flow is the gold standard for server-to-server Salesforce integrations. It uses asymmetric cryptography (private key signs the JWT; public certificate validates it) so no shared secret is transmitted. [src2, src6]

ClaimValueRequired?Notes
issConnected App consumer key (client_id)YesIdentifies which Connected App/External Client App [src2]
subSalesforce username of integration userYesAll API calls execute in this user's context [src2]
audhttps://login.salesforce.com or https://test.salesforce.comYesUse test.salesforce.com for sandboxes [src2]
expExpiration (Unix timestamp, seconds)YesMust be <= 5 minutes from now [src2]
iatIssued-at (Unix timestamp, seconds)OptionalHelps with clock skew debugging [src2]

Signing Algorithm: RS256 (RSA with SHA-256) [src2]

Client Credentials Flow -- Detailed Setup

Simpler than JWT Bearer because no certificate is needed -- uses client_id + client_secret instead. Introduced in Spring '23. [src3]

ParameterValueNotes
grant_typeclient_credentialsFixed string [src3]
client_idConnected App consumer keyFrom Connected App settings [src3]
client_secretConnected App consumer secretMust be stored securely; rotate if compromised [src3]

Execution User: Must designate a "Run As" user in the Connected App OAuth policies. All API calls execute as this user regardless of which client_id/secret is used. [src3]

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- User needs to authenticate with Salesforce API
|-- Is this server-to-server (no user interaction)?
|   |-- YES
|   |   |-- Can you manage X.509 certificates?
|   |   |   |-- YES --> JWT Bearer flow (recommended, most secure)
|   |   |   |   |-- Generate RSA key pair (2048-bit minimum)
|   |   |   |   |-- Upload public cert to Connected App/External Client App
|   |   |   |   |-- Sign JWT with private key, POST to /services/oauth2/token
|   |   |   |   '-- No refresh token; issue new JWT for each session
|   |   |   '-- NO --> Client Credentials flow (simpler)
|   |   |       |-- Create Connected App with client_id + client_secret
|   |   |       |-- Designate execution (Run As) user
|   |   |       |-- POST client_id + client_secret to /services/oauth2/token
|   |   |       '-- No refresh token; request new token when expired
|   |   '-- Need to impersonate specific users?
|   |       |-- YES --> JWT Bearer (set sub claim to target username)
|   |       '-- NO --> Client Credentials (always runs as execution user)
|   '-- NO (user interaction available)
|       |-- Is the client a web server (can store secrets)?
|       |   |-- YES --> Web Server (Authorization Code) flow
|       |   |   |-- Redirect user to Salesforce login
|       |   |   |-- Exchange auth code for access + refresh tokens
|       |   |   '-- Use refresh token for long-lived sessions
|       |   '-- NO (SPA, mobile, public client)
|       |       '-- PKCE (Authorization Code + PKCE) flow
|       |           |-- Generate code_verifier and code_challenge
|       |           |-- Redirect user with code_challenge
|       |           '-- Exchange code + code_verifier for tokens
'-- NEVER use Username-Password flow in production
    '-- NEVER use Device Flow (removed Sept 2025)

Quick Reference

OAuth FlowToken EndpointGrant Type ParameterKey CredentialToken Response
JWT BearerPOST /services/oauth2/tokenurn:ietf:params:oauth:grant-type:jwt-bearerassertion (signed JWT)access_token, instance_url, token_type
Client CredentialsPOST /services/oauth2/tokenclient_credentialsclient_id + client_secretaccess_token, instance_url, token_type
Web Server (Auth Code)POST /services/oauth2/tokenauthorization_codecode + client_id + client_secretaccess_token, refresh_token, instance_url
PKCEPOST /services/oauth2/tokenauthorization_codecode + code_verifier + client_idaccess_token, refresh_token, instance_url
Refresh TokenPOST /services/oauth2/tokenrefresh_tokenrefresh_token + client_id + client_secretaccess_token, instance_url
Username-PasswordPOST /services/oauth2/tokenpasswordusername + password + client_id + client_secretaccess_token, instance_url

Step-by-Step Integration Guide

1. Create an External Client App (Spring '26 recommended path)

Since Spring '26 disables Connected App creation by default, the recommended path is to create an External Client App via metadata. [src4, src8]

<!-- force-app/main/default/externalClientApps/MyIntegration.eca-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<ExternalClientApp xmlns="http://soap.sforce.com/2006/04/metadata">
    <label>My Integration App</label>
    <contactEmail>[email protected]</contactEmail>
    <description>Server-to-server integration via JWT Bearer</description>
    <oauthConfig>
        <callbackUrl>https://login.salesforce.com/services/oauth2/success</callbackUrl>
        <scopes>api</scopes>
        <scopes>refresh_token</scopes>
        <isSecretRequired>false</isSecretRequired>
        <certificate>MyCertificateName</certificate>
    </oauthConfig>
</ExternalClientApp>

Verify: Deploy with sf project deploy start and confirm the app appears in Setup > App Manager.

2. Generate an X.509 certificate for JWT Bearer flow

Create an RSA key pair and self-signed certificate. For production, use a CA-signed certificate. [src2]

# Input:  OpenSSL installed
# Output: private.key (keep secret) + public.crt (upload to Salesforce)

# Generate 2048-bit RSA private key
openssl genrsa -out private.key 2048

# Generate self-signed X.509 certificate (valid 365 days)
openssl req -new -x509 -key private.key -out public.crt -days 365 \
  -subj "/CN=SalesforceJWTIntegration/O=YourCompany"

Verify: openssl x509 -in public.crt -text -noout shows certificate details with correct CN.

3. Authenticate via JWT Bearer flow

Construct a JWT, sign it with your private key, and exchange it for an access token. [src2]

# Input:  consumer_key (from Connected App), private.key, username
# Output: access_token + instance_url

# Step 1: Create JWT header + payload (in practice, use a JWT library)
# Header: {"alg": "RS256"}
# Payload: {"iss": "<consumer_key>", "sub": "<username>",
#           "aud": "https://login.salesforce.com", "exp": <now+300>}

# Step 2: Exchange JWT for access token
curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=<signed_jwt_token>"

# Expected response:
# {"access_token":"00D...","scope":"api","instance_url":"https://yourorg.my.salesforce.com","id":"https://login.salesforce.com/id/00Dxx.../005xx...","token_type":"Bearer"}

Verify: Response contains access_token and instance_url. HTTP 200 = success.

4. Authenticate via Client Credentials flow

Simpler alternative when certificate management is not feasible. [src3]

# Input:  client_id + client_secret from Connected App
# Output: access_token + instance_url

curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=client_credentials" \
  -d "client_id=<consumer_key>" \
  -d "client_secret=<consumer_secret>"

# Expected response:
# {"access_token":"00D...","instance_url":"https://yourorg.my.salesforce.com","id":"https://login.salesforce.com/id/00Dxx.../005xx...","token_type":"Bearer"}

Verify: Response contains access_token. Use it in subsequent API calls with Authorization: Bearer <access_token>.

5. Implement token refresh (Web Server flow only)

For flows that return refresh tokens, implement automatic refresh before the access token expires. [src1, src7]

# Input:  refresh_token + client_id + client_secret
# Output: new access_token

curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=<refresh_token>" \
  -d "client_id=<consumer_key>" \
  -d "client_secret=<consumer_secret>"

Verify: Response contains a new access_token. The refresh_token itself may or may not be rotated (depends on Connected App policy).

6. Validate the token and check remaining API limits

After obtaining a token, verify it works and check your API quota. [src1]

# Input:  access_token + instance_url
# Output: API limits for the org

curl -H "Authorization: Bearer <access_token>" \
  https://<instance_url>/services/data/v66.0/limits

# Key fields in response:
# "DailyApiRequests": {"Max": 100000, "Remaining": 99850}

Verify: HTTP 200 with valid JSON. Check DailyApiRequests.Remaining > 0.

Code Examples

Python: JWT Bearer flow with automatic token management

# Input:  consumer_key, private_key_path, username, login_url
# Output: Salesforce access_token + instance_url

import jwt  # PyJWT>=2.8.0
import time
import requests  # requests>=2.31.0

def get_salesforce_token_jwt(consumer_key, private_key_path, username,
                              login_url="https://login.salesforce.com"):
    """Authenticate to Salesforce using OAuth 2.0 JWT Bearer flow."""
    with open(private_key_path, "r") as f:
        private_key = f.read()

    # Build JWT claims
    now = int(time.time())
    claims = {
        "iss": consumer_key,        # Connected App consumer key
        "sub": username,             # Salesforce integration user
        "aud": login_url,            # login.salesforce.com or test.salesforce.com
        "exp": now + 300,            # 5 minutes max (Salesforce hard limit)
    }

    # Sign with RS256
    assertion = jwt.encode(claims, private_key, algorithm="RS256")

    # Exchange JWT for access token
    resp = requests.post(
        f"{login_url}/services/oauth2/token",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "assertion": assertion,
        },
    )

    if resp.status_code != 200:
        raise Exception(f"JWT auth failed: {resp.status_code} -- {resp.text}")

    data = resp.json()
    return data["access_token"], data["instance_url"]

JavaScript/Node.js: Client Credentials flow

// Input:  SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET env vars
// Output: { accessToken, instanceUrl }

const https = require("https"); // built-in
const querystring = require("querystring"); // built-in

async function getSalesforceToken() {
  const params = querystring.stringify({
    grant_type: "client_credentials",
    client_id: process.env.SALESFORCE_CLIENT_ID,
    client_secret: process.env.SALESFORCE_CLIENT_SECRET,
  });

  return new Promise((resolve, reject) => {
    const req = https.request(
      "https://login.salesforce.com/services/oauth2/token",
      { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } },
      (res) => {
        let body = "";
        res.on("data", (chunk) => (body += chunk));
        res.on("end", () => {
          if (res.statusCode !== 200) {
            return reject(new Error(`Auth failed: ${res.statusCode} -- ${body}`));
          }
          const data = JSON.parse(body);
          resolve({ accessToken: data.access_token, instanceUrl: data.instance_url });
        });
      }
    );
    req.on("error", reject);
    req.write(params);
    req.end();
  });
}

cURL: Quick test of each flow

# Input:  Connected App credentials
# Output: access_token for API calls

# --- JWT Bearer flow ---
# (Requires a pre-built signed JWT assertion)
curl -s -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT_ASSERTION}" | jq .

# --- Client Credentials flow ---
curl -s -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=client_credentials" \
  -d "client_id=${SF_CLIENT_ID}" \
  -d "client_secret=${SF_CLIENT_SECRET}" | jq .

# --- Username-Password flow (testing only, not for production) ---
curl -s -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=password" \
  -d "client_id=${SF_CLIENT_ID}" \
  -d "client_secret=${SF_CLIENT_SECRET}" \
  -d "username=${SF_USERNAME}" \
  -d "password=${SF_PASSWORD}${SF_SECURITY_TOKEN}" | jq .

# Expected success response shape:
# {
#   "access_token": "00Dxx...",
#   "instance_url": "https://yourorg.my.salesforce.com",
#   "id": "https://login.salesforce.com/id/00Dxx.../005xx...",
#   "token_type": "Bearer",
#   "issued_at": "1709251200000"
# }

Data Mapping

Token Response Field Mapping

Response FieldTypePresent InNotes
access_tokenStringAll flowsOpaque token -- use as-is in Authorization header [src1]
instance_urlStringAll flowsBase URL for all subsequent API calls -- never hardcode [src1]
refresh_tokenStringWeb Server, PKCE onlyUse to obtain new access_token without re-auth [src1]
token_typeStringAll flowsAlways "Bearer" [src1]
idString (URL)All flowsIdentity URL -- GET to retrieve user info [src1]
issued_atString (epoch ms)All flowsMilliseconds since Unix epoch [src1]
scopeStringJWT Bearer, Client CredentialsSpace-separated list of granted scopes [src2, src3]
signatureStringAll flowsHMAC-SHA256 of id + issued_at using consumer secret [src1]

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

ErrorMeaningCauseResolution
invalid_grantJWT assertion invalid or expiredClock skew >5 min, wrong aud, revoked certificate, or user not pre-authorizedSync server clock via NTP; verify aud matches environment; check Connected App pre-authorization [src2]
invalid_client_idConsumer key not foundWrong client_id or Connected App not deployed to this orgVerify consumer key in Setup > App Manager [src1]
invalid_clientClient authentication failedWrong client_secret, or secret was rotatedRe-copy consumer secret from Connected App settings [src3]
unsupported_grant_typeGrant type not enabledClient Credentials not checked, or flow not supported for this appEnable the specific flow in Connected App OAuth settings [src3]
inactive_userIntegration user is inactive or frozenUser account deactivated or locked outReactivate user in Setup > Users [src2]
inactive_orgOrg is locked, suspended, or restrictedBilling, compliance, or admin lockContact Salesforce support [src1]
INVALID_LOGINUsername or password incorrectWrong credentials in Username-Password flowVerify username + password + security token [src1]
redirect_uri_mismatchCallback URL mismatchWeb Server flow redirect_uri doesn't match Connected AppUpdate callback URL in Connected App settings [src1]

Failure Points in Production

Anti-Patterns

Wrong: Hardcoding the Salesforce instance URL

# BAD -- instance URL can change if org is migrated
BASE_URL = "https://na1.salesforce.com"  # Legacy hostname, removed in Spring '26
resp = requests.get(f"{BASE_URL}/services/data/v66.0/query?q=...", headers=headers)

Correct: Always use instance_url from the token response

# GOOD -- dynamic instance URL from authentication response
access_token, instance_url = get_salesforce_token_jwt(key, cert, user)
resp = requests.get(f"{instance_url}/services/data/v66.0/query?q=...",
                    headers={"Authorization": f"Bearer {access_token}"})

Wrong: Caching access tokens with hardcoded expiry

# BAD -- assumes 2-hour token lifetime; admin can change session timeout
TOKEN_CACHE = {"token": None, "expires": 0}

def get_token():
    if time.time() < TOKEN_CACHE["expires"]:
        return TOKEN_CACHE["token"]
    token = authenticate()
    TOKEN_CACHE["token"] = token
    TOKEN_CACHE["expires"] = time.time() + 7200  # hardcoded 2 hours
    return token

Correct: Handle 401 with automatic re-authentication

# GOOD -- defensive re-authentication on token expiry
def api_call(instance_url, token, method, endpoint, **kwargs):
    resp = requests.request(method, f"{instance_url}{endpoint}",
                            headers={"Authorization": f"Bearer {token}"}, **kwargs)
    if resp.status_code == 401:  # token expired or revoked
        token, instance_url = get_salesforce_token_jwt(key, cert, user)
        resp = requests.request(method, f"{instance_url}{endpoint}",
                                headers={"Authorization": f"Bearer {token}"}, **kwargs)
    resp.raise_for_status()
    return resp.json()

Wrong: Using Username-Password flow in production

# BAD -- credentials in plaintext, breaks with MFA, security risk
resp = requests.post("https://login.salesforce.com/services/oauth2/token", data={
    "grant_type": "password",
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,
    "username": "[email protected]",
    "password": "P@ssw0rd123SecurityToken456"  # credentials in code
})

Correct: Use JWT Bearer or Client Credentials for server-to-server

# GOOD -- no credentials transmitted; certificate-based authentication
import jwt
claims = {"iss": CLIENT_ID, "sub": "[email protected]",
          "aud": "https://login.salesforce.com", "exp": int(time.time()) + 300}
assertion = jwt.encode(claims, private_key, algorithm="RS256")
resp = requests.post("https://login.salesforce.com/services/oauth2/token", data={
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": assertion
})

Common Pitfalls

Diagnostic Commands

# Test JWT Bearer authentication
curl -v -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT_ASSERTION}"

# Test Client Credentials authentication
curl -v -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=client_credentials" \
  -d "client_id=${SF_CLIENT_ID}" \
  -d "client_secret=${SF_CLIENT_SECRET}"

# Check token validity and user identity
curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  https://${INSTANCE_URL}/services/oauth2/userinfo

# Check remaining API limits (confirms token works)
curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  https://${INSTANCE_URL}/services/data/v66.0/limits

# Introspect token (check expiration)
curl -X POST https://login.salesforce.com/services/oauth2/introspect \
  -d "token=${ACCESS_TOKEN}" \
  -d "client_id=${SF_CLIENT_ID}" \
  -d "client_secret=${SF_CLIENT_SECRET}" \
  -d "token_type_hint=access_token"

# Revoke a token (useful for testing token expiry handling)
curl -X POST https://login.salesforce.com/services/oauth2/revoke \
  -d "token=${ACCESS_TOKEN}"

# Verify certificate (check expiration date)
openssl x509 -in public.crt -enddate -noout

Version History & Compatibility

API VersionReleaseStatusKey Auth ChangesNotes
v66.0Spring '26 (Feb 2026)CurrentConnected App creation disabled by default; External Client Apps recommendedLegacy hostname redirects removed [src8]
v63.0Spring '25 (Feb 2025)SupportedUninstalled Connected Apps blocked; Device Flow removed (Sept 2025)"Use Any API Client" permission introduced [src5]
v58.0Summer '23 (Jun 2023)SupportedClient Credentials "API Only" user restriction removedAny user can be execution user [src3]
v57.0Spring '23 (Feb 2023)SupportedClient Credentials flow introduced (GA)New server-to-server option [src3]
v51.0Spring '21 (Feb 2021)SupportedToken exchange flow introducedFor IoT/asset token scenarios [src1]
v29.0Winter '14 (Oct 2013)EOLJWT Bearer flow introducedOriginal server-to-server flow [src2]
v21.0Spring '11EOLOAuth 2.0 support introducedWeb Server + User-Agent flows [src1]

Deprecation Policy

Salesforce supports API versions for a minimum of 3 years. API versions 21.0 through 30.0 were retired in June 2025. OAuth flows themselves are not versioned separately -- they depend on the Connected App/External Client App configuration, not the API version used in subsequent calls. However, the token endpoint URL and flow availability can change with major releases (e.g., Device Flow removal in Sept 2025, Connected App creation restriction in Spring '26). Always monitor Salesforce Release Notes before each major release. [src5, src8]

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Server-to-server automation (ETL, middleware, batch jobs)End-user SSO/login to Salesforce UISAML 2.0 or OpenID Connect
API calls must execute without user interactionMobile app needs user authenticationPKCE (Authorization Code + PKCE)
Integration needs to impersonate a specific userClient cannot manage certificates and needs simplicityClient Credentials flow
Building a web application with user-initiated Salesforce actionsOrg has MFA enforced and you need quick scriptingJWT Bearer or Client Credentials
Need long-lived access to Salesforce dataOne-time data exportSalesforce Data Export (Setup > Data Export)

Important Caveats

Related Units