Salesforce OAuth 2.0 Authentication Flows for API Integration
What OAuth 2.0 flows does Salesforce support and which should I use for server-to-server integration?
TL;DR
- Bottom line: Use JWT Bearer flow for server-to-server integrations (most secure, no client secret transmitted); use Client Credentials flow as a simpler alternative when certificate management is impractical; use Web Server (Authorization Code) flow when API calls must execute in a specific user's context. [src1, src2, src3]
- Key limit: JWT assertion
expclaim must be within 5 minutes of server time -- clock skew is the #1 cause of JWT auth failures. Connected App creation is disabled by default in Spring '26; use External Client Apps instead. [src2, src4, src8] - Watch out for: Username-Password flow breaks when MFA is enforced org-wide (mandatory since Feb 2022) -- migrate to JWT Bearer or Client Credentials immediately. OAuth Device Flow was permanently removed September 2025. [src1, src5, src6]
- Best for: Any Salesforce REST, SOAP, Bulk, or Composite API integration requiring programmatic authentication without browser-based user interaction. [src1]
- Authentication: JWT Bearer (recommended for server-to-server), Client Credentials (simpler server-to-server), Web Server/Authorization Code (user-context), PKCE (mobile/SPA). [src1, src2, src3, src6]
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]
| Property | Value |
|---|---|
| Vendor | Salesforce |
| System | Salesforce Platform (Spring '26) |
| API Surface | OAuth 2.0 Authentication |
| Current API Version | v66.0 (Spring '26) |
| Editions Covered | Enterprise, Unlimited, Developer, Performance |
| Deployment | Cloud |
| API Docs | OAuth Authorization Flows |
| Status | GA (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 Flow | Protocol | Best For | User Context? | Refresh Token? | Certificate Required? | Security Level |
|---|---|---|---|---|---|---|
| JWT Bearer | OAuth 2.0 | Server-to-server automation (ETL, middleware) | No (integration user) | No (new JWT per request) | Yes (X.509) | Highest |
| Client Credentials | OAuth 2.0 | Server-to-server (simpler setup) | No (execution user) | No (request new token) | No | High |
| Web Server (Auth Code) | OAuth 2.0 | User-context web applications | Yes | Yes | No | High |
| PKCE (Auth Code + PKCE) | OAuth 2.0 | Mobile apps, SPAs, public clients | Yes | Yes | No | High |
| User-Agent (Implicit) | OAuth 2.0 | Legacy SPAs (not recommended) | Yes | No | No | Low |
| Username-Password | OAuth 2.0 | Testing only (breaks with MFA) | Yes (specific user) | No | No | Very Low |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| JWT assertion validity window | 5 minutes max | JWT Bearer flow | exp claim must be <= 5 min from current time [src2] |
| Token endpoint rate limit | Shared with org API limits | All OAuth flows | Token requests count against 24h API call limit [src1] |
| Max concurrent sessions per user | 5 (default) | All flows | Configurable per profile; oldest session killed when exceeded [src7] |
| Access token size | ~1.5 KB | All flows | Opaque token, not a JWT -- do not parse [src1] |
Rolling / Daily Limits
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| API calls (includes token requests) | 100,000 base | 24h rolling | Enterprise: 100K + (user count x 1,000); Unlimited: 5M; Developer: 15K [src1] |
| Token refresh requests | Counts against API limit | Per request | Each refresh_token exchange = 1 API call [src7] |
| Connected App consent prompts | No hard limit | Per user | Excessive prompts may indicate misconfiguration [src1] |
Token Lifetime Policies
| Token Type | Default Lifetime | Configurable? | Policy Options |
|---|---|---|---|
| Access token | 2 hours (7,200s) | Yes (session timeout) | Tied to Connected App session policy [src7] |
| Refresh token | Until revoked (default) | Yes (4 policy types) | Until revoked / Expire after N / Expire if unused for N / Immediate expire [src7] |
| JWT assertion | 5 min max | No (hard limit) | Must be < 5 min from iat to exp [src2] |
Authentication
Flow Selection Matrix
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| JWT Bearer | Server-to-server, no user interaction, certificate management available | Session timeout (default 2h) | No -- issue new JWT each time | Most secure; recommended for production integrations [src2] |
| Client Credentials | Server-to-server, simpler setup, no certificate infrastructure | Session timeout (default 2h) | No -- request new token | Requires designated execution user in Connected App [src3] |
| Web Server (Auth Code) | User-context operations, interactive web apps | Access: 2h, Refresh: until revoked | Yes | Requires callback URL; most common for web apps [src1] |
| PKCE | Mobile apps, SPAs, public clients (cannot store secret) | Access: 2h, Refresh: until revoked | Yes | Replaces User-Agent flow; code_verifier prevents interception [src6] |
| Username-Password | Testing and development ONLY | Session timeout | No | Incompatible 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]
| Claim | Value | Required? | Notes |
|---|---|---|---|
iss | Connected App consumer key (client_id) | Yes | Identifies which Connected App/External Client App [src2] |
sub | Salesforce username of integration user | Yes | All API calls execute in this user's context [src2] |
aud | https://login.salesforce.com or https://test.salesforce.com | Yes | Use test.salesforce.com for sandboxes [src2] |
exp | Expiration (Unix timestamp, seconds) | Yes | Must be <= 5 minutes from now [src2] |
iat | Issued-at (Unix timestamp, seconds) | Optional | Helps 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]
| Parameter | Value | Notes |
|---|---|---|
grant_type | client_credentials | Fixed string [src3] |
client_id | Connected App consumer key | From Connected App settings [src3] |
client_secret | Connected App consumer secret | Must 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
- Connected App creation disabled by default in Spring '26: New orgs cannot create Connected Apps without admin explicitly enabling the feature. Use External Client Apps (metadata-based,
.eca-meta.xml) instead. Existing Connected Apps continue to work. [src4, src5, src8] - JWT clock skew kills integrations silently: If your server clock is >5 minutes off from Salesforce, every JWT assertion is rejected with
invalid_grant. Use NTP sync. Many cloud providers have clock drift on containers. [src2] - Client Credentials flow always uses execution user context: Even if you pass different client_id/secret pairs, the API calls run as the single designated execution user. You cannot impersonate different users with this flow. [src3]
- Refresh tokens expire after 90 days of non-use by default in some policies: If your integration runs less frequently than your refresh token inactivity timeout, the token expires and the integration breaks silently. [src7]
- Sandbox vs production token endpoints differ: JWT
audmust behttps://test.salesforce.comfor sandboxes. A common mistake is hardcoding the production endpoint and wondering why sandbox auth fails. [src2] - My Domain URLs required in Spring '26: Legacy hostname redirects (
na1.salesforce.com, etc.) were removed. Always use My Domain URL (yourorg.my.salesforce.com). [src8] - Session timeout is admin-configurable: The default 2-hour access token lifetime can be changed by a Salesforce admin to as short as 15 minutes. Do not hardcode token TTL. [src7]
Constraints
- Connected App creation disabled by default in Spring '26 -- existing Connected Apps still work, but new integrations should use External Client Apps (metadata-driven). Admins can re-enable Connected App creation if needed. [src4, src5, src8]
- JWT Bearer requires X.509 certificate -- self-signed works for development, but production integrations should use CA-signed certificates with proper rotation procedures. Certificate must be uploaded to the Connected App/External Client App. [src2, src4]
- Client Credentials flow requires a designated execution user -- all API calls run in this user's security context. The user must have the appropriate profile and permission sets for the data being accessed. In Spring '23, the user had to be "API Only"; this restriction was removed in Summer '23. [src3]
- OAuth Device Flow permanently removed September 2025 -- no exceptions, no extensions. Migrate to PKCE or Web Server flow. [src5]
- Username-Password flow incompatible with MFA -- since Salesforce enforced mandatory MFA (Feb 2022), this flow breaks for any org with MFA enabled. It is unsuitable for production. [src1, src6]
- Uninstalled Connected Apps blocked since September 2025 -- unless users have "Approve Uninstalled Connected Apps" or "Use Any API Client" permission. [src5]
- Refresh token policies are admin-controlled -- an admin can change the policy from "valid until revoked" to "expire after 24 hours" without notifying integration teams. Build defensively. [src7]
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 Flow | Token Endpoint | Grant Type Parameter | Key Credential | Token Response |
|---|---|---|---|---|
| JWT Bearer | POST /services/oauth2/token | urn:ietf:params:oauth:grant-type:jwt-bearer | assertion (signed JWT) | access_token, instance_url, token_type |
| Client Credentials | POST /services/oauth2/token | client_credentials | client_id + client_secret | access_token, instance_url, token_type |
| Web Server (Auth Code) | POST /services/oauth2/token | authorization_code | code + client_id + client_secret | access_token, refresh_token, instance_url |
| PKCE | POST /services/oauth2/token | authorization_code | code + code_verifier + client_id | access_token, refresh_token, instance_url |
| Refresh Token | POST /services/oauth2/token | refresh_token | refresh_token + client_id + client_secret | access_token, instance_url |
| Username-Password | POST /services/oauth2/token | password | username + password + client_id + client_secret | access_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 Field | Type | Present In | Notes |
|---|---|---|---|
access_token | String | All flows | Opaque token -- use as-is in Authorization header [src1] |
instance_url | String | All flows | Base URL for all subsequent API calls -- never hardcode [src1] |
refresh_token | String | Web Server, PKCE only | Use to obtain new access_token without re-auth [src1] |
token_type | String | All flows | Always "Bearer" [src1] |
id | String (URL) | All flows | Identity URL -- GET to retrieve user info [src1] |
issued_at | String (epoch ms) | All flows | Milliseconds since Unix epoch [src1] |
scope | String | JWT Bearer, Client Credentials | Space-separated list of granted scopes [src2, src3] |
signature | String | All flows | HMAC-SHA256 of id + issued_at using consumer secret [src1] |
Data Type Gotchas
issued_atis a string of epoch milliseconds (not seconds) -- divide by 1000 for Unix timestamp comparison. [src1]instance_urlmay change between token requests if the org is migrated to a different Salesforce instance. Always use theinstance_urlfrom the most recent token response. [src1]access_tokenis opaque -- do not attempt to decode it as a JWT. Its format and length can change without notice. [src1]refresh_tokenfrom Web Server flow may be a new token on each refresh (token rotation) depending on Connected App policy. Always store the latest refresh token. [src7]
Error Handling & Failure Points
Common Error Codes
| Error | Meaning | Cause | Resolution |
|---|---|---|---|
invalid_grant | JWT assertion invalid or expired | Clock skew >5 min, wrong aud, revoked certificate, or user not pre-authorized | Sync server clock via NTP; verify aud matches environment; check Connected App pre-authorization [src2] |
invalid_client_id | Consumer key not found | Wrong client_id or Connected App not deployed to this org | Verify consumer key in Setup > App Manager [src1] |
invalid_client | Client authentication failed | Wrong client_secret, or secret was rotated | Re-copy consumer secret from Connected App settings [src3] |
unsupported_grant_type | Grant type not enabled | Client Credentials not checked, or flow not supported for this app | Enable the specific flow in Connected App OAuth settings [src3] |
inactive_user | Integration user is inactive or frozen | User account deactivated or locked out | Reactivate user in Setup > Users [src2] |
inactive_org | Org is locked, suspended, or restricted | Billing, compliance, or admin lock | Contact Salesforce support [src1] |
INVALID_LOGIN | Username or password incorrect | Wrong credentials in Username-Password flow | Verify username + password + security token [src1] |
redirect_uri_mismatch | Callback URL mismatch | Web Server flow redirect_uri doesn't match Connected App | Update callback URL in Connected App settings [src1] |
Failure Points in Production
- Clock skew on containerized servers: Docker containers, Kubernetes pods, and serverless functions can drift >5 minutes from NTP, causing every JWT assertion to fail with
invalid_grant. Fix:configure NTP sync in container images; use cloud provider's time sync service (AWS: chrony, GCP: metadata server, Azure: VMICTimeProvider). [src2] - Refresh token silent expiration: If a refresh token policy is set to "expire if unused for N days" and the integration runs less frequently than N days, the integration breaks with no warning. Fix:
implement a health check that refreshes the token at least once within the inactivity window; alert on consecutive 401 responses. [src7] - Connected App secret rotation breaks all clients: Rotating the consumer secret in a Connected App immediately invalidates all client_secret-based flows. Fix:
use JWT Bearer flow (certificate-based) for production; when rotating secrets, update all clients before regenerating. [src3] - My Domain URL change breaks hardcoded instance_url: If a Salesforce admin changes the My Domain name, hardcoded instance URLs break. Fix:
always use the instance_url from the token response; never hardcode it. [src8] - Admin changes session timeout without notifying integrations: A Salesforce admin can change the session timeout from 2 hours to 15 minutes, causing tokens to expire much faster than expected. Fix:
implement proactive token refresh at 75% of expected TTL; handle 401 responses with automatic re-authentication. [src7] - Certificate expiration breaks JWT flow silently: X.509 certificates have expiration dates. When the certificate expires, JWT auth fails with
invalid_grant. Fix:set calendar reminders for certificate renewal; implement certificate monitoring; use short-lived certificates (90 days) with automated rotation. [src2, src4]
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
- Sandbox uses different token endpoint: Sandbox orgs require
https://test.salesforce.com/services/oauth2/token. Using the production endpoint returnsinvalid_grant. Also set the JWTaudclaim tohttps://test.salesforce.com. [src2] - Security token appended to password: Username-Password flow requires the security token appended to the password (e.g.,
password + securityToken). If the user's IP is in a trusted range, the token is not required -- but this varies and breaks when the user changes location. [src1] - Not pre-authorizing the integration user for JWT Bearer: JWT Bearer flow requires the integration user to be pre-authorized in the Connected App (Setup > Manage Connected Apps > your app > Manage > select profiles or permission sets). Without this, auth fails with
user hasn't approved this consumer. [src2] - Storing consumer secret in source control: Client Credentials flow requires a consumer secret. Committing it to git exposes the integration. Fix: use environment variables or a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). [src3]
- Not handling token endpoint errors programmatically: The token endpoint returns JSON error responses, not HTTP error codes alone. Parse the
erroranderror_descriptionfields for debugging. [src1, src2] - Ignoring the
idURL in the token response: The identity URL (idfield) can beGET-requested to retrieve user details, org info, and verify the integration user's identity. Useful for debugging. [src1]
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 Version | Release | Status | Key Auth Changes | Notes |
|---|---|---|---|---|
| v66.0 | Spring '26 (Feb 2026) | Current | Connected App creation disabled by default; External Client Apps recommended | Legacy hostname redirects removed [src8] |
| v63.0 | Spring '25 (Feb 2025) | Supported | Uninstalled Connected Apps blocked; Device Flow removed (Sept 2025) | "Use Any API Client" permission introduced [src5] |
| v58.0 | Summer '23 (Jun 2023) | Supported | Client Credentials "API Only" user restriction removed | Any user can be execution user [src3] |
| v57.0 | Spring '23 (Feb 2023) | Supported | Client Credentials flow introduced (GA) | New server-to-server option [src3] |
| v51.0 | Spring '21 (Feb 2021) | Supported | Token exchange flow introduced | For IoT/asset token scenarios [src1] |
| v29.0 | Winter '14 (Oct 2013) | EOL | JWT Bearer flow introduced | Original server-to-server flow [src2] |
| v21.0 | Spring '11 | EOL | OAuth 2.0 support introduced | Web 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 When | Don't Use When | Use Instead |
|---|---|---|
| Server-to-server automation (ETL, middleware, batch jobs) | End-user SSO/login to Salesforce UI | SAML 2.0 or OpenID Connect |
| API calls must execute without user interaction | Mobile app needs user authentication | PKCE (Authorization Code + PKCE) |
| Integration needs to impersonate a specific user | Client cannot manage certificates and needs simplicity | Client Credentials flow |
| Building a web application with user-initiated Salesforce actions | Org has MFA enforced and you need quick scripting | JWT Bearer or Client Credentials |
| Need long-lived access to Salesforce data | One-time data export | Salesforce Data Export (Setup > Data Export) |
Important Caveats
- Connected App creation restriction in Spring '26 is the most impactful recent change -- ensure your team knows about External Client Apps as the replacement path. Existing Connected Apps are unaffected. [src4, src5, src8]
- OAuth Device Flow was permanently removed September 2025 with no exceptions. Any integration still using it must migrate immediately. [src5]
- Token lifetimes and refresh token policies are admin-configurable and can change without notice to integration teams. Build defensive token management that handles unexpected 401 responses. [src7]
- Sandbox and production use different token endpoints (
test.salesforce.comvslogin.salesforce.com). Parameterize the login URL in your integration configuration. [src2] - The Client Credentials flow always runs as a single designated execution user. It cannot provide per-user context like JWT Bearer (which accepts different
subvalues). [src3] - Rate limits are subject to change with each Salesforce release. Always verify against the current Salesforce Developer Limits Quick Reference. [src1]