Skip to main content

Salesforce OAuth 2.0 Flows: PKCE, JWT, Client Credentials

Updated Add a comment
⚡ 1-Minute Summary
  • The Golden Question: One simple question picks your OAuth 2.0 flow: Is a human sitting in front of a screen, or is a machine talking to a machine?
  • Identity Resilience: Always enforce Multi-Factor Authentication (MFA) at your Single Sign-On (SSO) Identity Provider, and keep one dedicated "break-glass" System Administrator account outside of SSO in case your IdP goes down.
  • How Token Power Works: An access token's effective permission is the intersection of the OAuth Scope AND the User's Salesforce Profile/Permission Sets—never one by itself.
💬 In plain words: OAuth 2.0 is how external applications connect to Salesforce without ever seeing or storing your password. Instead of handing over a password, Salesforce gives the external app a temporary digital hotel key card called an Access Token. Different scenarios require different ways of getting that key card: Web Server Flow with PKCE is for web and mobile apps where a human logs in; JWT Bearer Flow is for automated server-to-server jobs using a digital certificate; Client Credentials Flow is for pure machine-to-machine integrations; and Device Flow is for smart TVs or CLI tools without a browser. Match the flow to the scenario, and integration security becomes easy.
📌 Example:
• Field Service Mobile App on iOS/Android: Use Authorization Code Flow with PKCE (code_challenge) so mobile devices never store a static client secret.
• Nightly GitHub Actions CI/CD Pipeline or ERP Batch Sync: Use OAuth 2.0 JWT Bearer Flow signed with an X.509 certificate—zero human interaction required.
• Marketing SaaS App Where Users Click "Connect to Salesforce": Use the Web Server Flow (Authorization Code Flow with PKCE).
• Red Flag Alert: Anyone proposing the legacy Username-Password Flow in 2026 is creating a massive security hole—it is officially deprecated and blocked in modern External Client Apps.
🎬 Real-Life Example: The Expired Password That Crashed Six Integrations at Midnight

A few years ago, I joined an enterprise project where the company's ERP, data warehouse, and billing middleware all authenticated into Salesforce using the legacy OAuth 2.0 Username-Password Flow. Every single integration shared one regular admin user account's username, password, and concatenated security token stored inside plain-text configuration files.

The Old/Bad Way: Because that user account was assigned to a standard profile with a 90-day password expiration policy, the password expired at midnight on a Sunday. Instantly, six mission-critical middleware jobs failed simultaneously. Even worse, when a contractor left the team two months later, we had to reset that shared password and manually coordinate downtime across six separate engineering teams to update their .env files.

Why this is bad: Storing passwords and security tokens in config files violates modern security compliance, fails whenever MFA is enforced on interactive logins, offers zero granular scoping per integration, and makes zero-downtime credential rotation impossible.

The New/Good Way: I migrated our automated pipelines and middleware away from shared passwords to External Client Apps (and Connected Apps) using the OAuth 2.0 JWT Bearer Flow and Client Credentials Flow paired with dedicated Salesforce Integration user licenses and Minimum Access - API Only Integrations profiles. Each middleware system received its own X.509 digital certificate and was restricted strictly to the api OAuth scope.

The Payoff: No integration ever broke due to a 90-day password reset again. When security asked us to rotate credentials every quarter, we simply uploaded the new certificate to the app definition, flipped the middleware key, and deleted the old certificate—achieving 100% zero-downtime rotation.
🧠 Token, Not Password: OAuth hands the app a scoped, revocable token instead of your password. Who holds the secret—and whether a human is on screen—picks the exact flow.

🔑 Key Points

  • One Question Decides the Flow: Ask yourself: Is a human sitting at a screen? If yes, use Authorization Code Flow with PKCE. If no (headless server-to-server), use JWT Bearer Flow or Client Credentials Flow.
  • Retire Legacy Flows Immediately: Both the Username-Password Flow and the User-Agent (Implicit) Flow are legacy anti-patterns. Salesforce is retiring the Username-Password flow (Winter '27 milestone), and modern External Client Apps block it completely.
  • Always Require PKCE: Proof Key for Code Exchange (PKCE) is no longer just for mobile apps—Salesforce now recommends (and lets you enforce via checkbox) PKCE on all Web Server / Authorization Code flows to prevent authorization code interception attacks.
  • Intersection of Authority: Giving a Connected App or External Client App the full or api scope never grants extra data access beyond what the authenticating user's Profile, Permission Sets, and Sharing Rules already allow.
  • External Client Apps (ECA) are the Future: While classic Connected Apps still work, Salesforce's modern External Client Apps provide second-generation packaging (2GP) support, clean metadata separation between developer config and admin policies, and built-in secret rotation.

⚙️ Concept: Matching Every OAuth 2.0 Flow to Its Architecture

Every OAuth 2.0 flow in Salesforce is designed around two fundamental constraints: Can the client application safely hide a secret key? and Is a human user present to click "Allow" in a browser? Here is how the core flows break down:

  • Web Server Flow (Authorization Code Flow + PKCE): Built for web applications running on a secure backend server—or mobile/single-page apps when paired with PKCE. The user logs in via a Salesforce browser redirect, Salesforce returns a one-time short-lived authorization_code, and the backend exchanges that code (plus its client_secret and code_verifier) server-to-server for an access_token and refresh_token.
  • User-Agent Flow (Legacy Implicit Flow): The old browser-based flow that returned an access_token directly inside the URL hash fragment (#access_token=...). Because browser history, referrer headers, and malicious browser extensions can steal tokens from URL fragments, this flow is superseded by Authorization Code Flow with PKCE.
  • JWT Bearer Flow (Headless Server-to-Server with User Context): Built for unattended automation—like CI/CD pipelines, ETL jobs, and backend microservices—where no human is present. The external server constructs a JSON Web Token (JWT) specifying a target Salesforce username, signs it cryptographically using a private RSA SHA256 key, and sends it to Salesforce. Salesforce verifies the signature against the uploaded X.509 certificate and returns an access_token immediately. Note: JWT Bearer never issues a refresh token; when the access token expires, the server simply signs a fresh JWT.
  • OAuth 2.0 Client Credentials Flow (Pure Machine-to-Machine): Standardized in modern Salesforce releases, this allows an external backend server to exchange its client_id and client_secret directly for an access_token. Inside the Connected App or External Client App policies, the admin maps the app to a specific Run As integration user.
  • Refresh Token Flow (Session Continuity): Access tokens expire quickly (often 15 minutes to 2 hours depending on session settings). Rather than forcing a human user to log in again every hour, interactive flows issue a long-lived refresh_token (when the refresh_token / offline_access scope is requested) so the app can silently fetch a fresh access token behind the scenes.
  • Device Authentication Flow: Built for input-constrained devices without a proper browser (like smart TVs, IoT kiosks, or terminal CLI utilities). The device displays a short alphanumeric code and tells the user to visit a verification URL on their phone or laptop to approve access.

Here is what the payload structure and token request look like for the industry-standard OAuth 2.0 JWT Bearer Flow:

// 1. JWT Header (Base64Url Encoded)
{
  "alg": "RS256",
  "typ": "JWT"
}

// 2. JWT Claims Payload (Base64Url Encoded & Signed with Private Key)
{
  "iss": "3MVG9SemV5D80oB_YOUR_CONNECTED_APP_CONSUMER_KEY",
  "sub": "cicd-bot@vtfriday.com",
  "aud": "https://login.salesforce.com",
  "exp": 1792848900
}
# 3. POST Request to Salesforce Token Endpoint (Always use your My Domain URL or login/test.salesforce.com)
curl -X POST https://vtfriday.my.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzTVZ..."
⚠️ Common Architect Trap: The JWT aud (Audience) Mismatch & Token Scope Myth
Trap 1 (Audience Claim Failure): When configuring the JWT Bearer Flow, the aud claim inside your signed JSON payload must strictly match standard Salesforce login endpoints: https://login.salesforce.com (for Production/Developer Edition), https://test.salesforce.com (for Sandboxes), or your exact Experience Cloud site URL—even when you send the actual HTTP POST call to https://yourcompany.my.salesforce.com/services/oauth2/token! Passing your custom My Domain URL inside the aud claim is the #1 cause of invalid_grant: invalid assertion errors.

Trap 2 (The Scope Myth): Many developers assume that granting the full scope on a Connected App gives an integration God-mode access. False! OAuth scope is merely a filter on what the application is allowed to ask for on behalf of the user. If the user only has Read access to Account via their Permission Set, a token with full scope still cannot edit an Account.
🧭 360 Card — Salesforce OAuth 2.0 Flows

Rule: One question decides the flow: Is a human present? Human on screen → Authorization Code Flow with PKCE. No human present → JWT Bearer Flow or Client Credentials Flow.
Gain: External applications never touch or store a Salesforce password. Instead, they hold a scoped token that you can monitor, expire, and revoke instantly.
Reach For: • Human on a web/mobile screen: Authorization Code Flow with PKCE.
• Server acting as a specific pre-authorized user (CI/CD, middleware): JWT Bearer Flow.
• Server acting purely as itself: Client Credentials Flow.
• Smart TV or CLI tool without a browser: Device Flow.
• Never reach for: Username-Password Flow (deprecated, zero scoping, retiring in Winter '27).
Price: More moving infrastructure parts to manage—Connected Apps or External Client Apps, X.509 certificates, callback URLs, and token expiration/rotation lifecycles.
Limits: Authorization codes are single-use and expire in minutes. Access tokens are short-lived; refresh tokens last days or months depending on policy. JWT Bearer requires pre-authorized Permission Sets on the app and never issues a refresh token. Client Credentials runs strictly as one designated integration user.
Mirror (Username + Password + Security Token): Takes two minutes to set up initially, but leaves raw credentials sitting in config files, breaks under MFA policies, offers zero scope control, and forces you to lock out the entire user account during a breach.
Later: The OAuth flow you pick today dictates how painful your quarterly security rotation will be next year. Certificate-based flows overlap cleanly during rotation; static secrets require coordinated cutover windows.
At Volume: Unmonitored refresh_token grants are your biggest standing security liability. Always enforce Refresh Token Rotation, set explicit expiration policies on the Connected App, and never store refresh tokens in browser local storage.

💬 Core Architecture Q&A

Q: Match the right OAuth 2.0 flow to each scenario: (a) A CI/CD pipeline deploying to Production, (b) A customer-facing mobile app, (c) Middleware syncing ERP data as a system, and (d) A partner's server-side web application.
🎯 Say this first: "(a) JWT Bearer Flow, (b) Authorization Code Flow with PKCE, (c) Client Credentials Flow (or JWT Bearer), and (d) Web Server Flow with PKCE—always matching human-present vs. machine-only architecture."

A: Let's break down the architectural reasoning for each of the four scenarios:

  • (a) CI/CD Pipeline Deploying to Production → JWT Bearer Flow: A GitHub Actions or GitLab runner is completely headless—no human is sitting there to type a password or approve an MFA push notification. Using an X.509 private key stored in GitHub Secrets, the runner signs a JWT and authenticates against a pre-authorized External Client App or Connected App.
  • (b) Customer-Facing Mobile App → Authorization Code Flow with PKCE: Mobile binaries distributed to thousands of iPhones and Android devices are "public clients"—anyone can decompile a mobile app and steal a hardcoded client_secret. With PKCE (Proof Key for Code Exchange), the app dynamically generates a one-time cryptographic code_verifier and code_challenge for every login attempt, binding the authorization code strictly to that specific app instance. Pair this with Refresh Token Rotation for secure long-lived mobile sessions.
  • (c) Middleware Syncing Data as a System → Client Credentials Flow (or JWT Bearer): This is a pure machine-to-machine integration running under a dedicated Salesforce Integration license. Choose Client Credentials Flow if your enterprise vault manages client ID/secret pairs natively and only needs a single fixed integration user; choose JWT Bearer if your security team prefers certificate-based authentication or needs the middleware to dynamically impersonate different pre-authorized integration users via the sub claim.
  • (d) Partner's Server-Side Web App → Web Server Flow (Authorization Code + PKCE): Because the partner application runs on a secure backend server, it can safely store a client_secret while redirecting individual human users to Salesforce to log in and grant consent under their own user context.
  • What to Call Out as an Anti-Pattern: If anyone suggests putting a username and password in an environment variable for scenarios (a) or (c), flag it immediately. The OAuth 2.0 Username-Password Flow is officially deprecated, retires in Winter '27, and is completely unsupported in modern External Client Apps.
Q: Give me your one-line decision rule for picking a Salesforce OAuth 2.0 flow in an architecture review.
🎯 Say this first: "Is a human present? Human on screen means Authorization Code with PKCE; no human means JWT Bearer or Client Credentials."

A: Memorize this four-part mental map and you can answer any integration authentication question in seconds:

  • Human on a browser or mobile screen: Use Authorization Code Flow + PKCE (modern replacement for the old User-Agent flow).
  • Server acting unattended on behalf of a specific user: Use JWT Bearer Flow (proven via digital certificate).
  • Server acting unattended purely as itself: Use Client Credentials Flow (mapped to one integration user on the app policy).
  • Device with a screen but no web browser (Smart TV / CLI): Use Device Authentication Flow.
Q: Why does the JWT Bearer Flow never show a login or consent screen to a user?

A: Because trust and consent are configured ahead of time by the Salesforce Administrator! In a standard interactive flow, Salesforce shows an "Allow Access?" screen because it needs the human user to verify that they trust the app. In the JWT Bearer Flow, three things establish that trust administratively:

  1. The external server holds the private key, and the Salesforce Connected App / External Client App holds the matching public X.509 certificate.
  2. The app's OAuth policy is explicitly set to Admin approved users are pre-authorized.
  3. The target integration user's Permission Set (or Profile) is explicitly assigned to the app.

When Salesforce receives the cryptographically signed JWT assertion, it already knows the app is authentic and the user is pre-approved—so it issues the access_token immediately without any interactive prompt.

Q: Where do Refresh Tokens fit into your architecture, and what security policies should you enforce on them?

A: Interactive flows (like Web Server and Mobile PKCE) issue a refresh_token alongside the short-lived access_token so the application can silently obtain a new access token when the current one expires—without kicking the user back to the login screen every 30 minutes.

Because a refresh token acts like a long-lived master key, you must lock it down with four strict policies:

  • 1. Explicit Scoping: Only grant the refresh_token (or offline_access) scope to apps that genuinely need background session continuity.
  • 2. Expire Refresh Tokens: Never leave the Connected App policy set to "Refresh token is valid until revoked" unless strictly necessary. Set it to Expire refresh token if not used for X days (e.g., 30 days) or expire it after a fixed lifecycle.
  • 3. Enforce Refresh Token Rotation: Enable OAuth 2.0 Refresh Token Rotation so every time a refresh token is exchanged for a new access token, Salesforce issues a brand-new refresh token and invalidates the old one immediately—neutralizing stolen token replay attacks.
  • 4. Incident Response: Store refresh tokens strictly server-side (or inside hardware-backed mobile keychains—never in browser localStorage). During a security incident, revoking active tokens via Setup → Connected Apps OAuth Usage is the fastest way to cut off a compromised external app without deactivating human users.

🔄 Follow-Up Scenarios (Connecting the Dots)

Q1: Walk through the exact mechanics of the JWT Bearer Flow and the four common failure points that break it in production.

A1: Under the hood, the client constructs a JSON payload containing four mandatory claims: iss (the Connected App's Consumer Key / Client ID), sub (the Salesforce Username of the integration user), aud (the Salesforce authentication server URL, such as https://login.salesforce.com or https://test.salesforce.com), and exp (an expiration timestamp in Unix epoch seconds, maximum 3 to 5 minutes in the future). The client signs this payload with its private key, POSTs it to /services/oauth2/token, and receives a bearer access_token.

When a JWT Bearer integration fails at 2:00 AM, check these four failure points first:

  • 1. Missing Pre-Authorization: If the Connected App policy is still set to "All users may self-authorize" instead of "Admin approved users are pre-authorized" (with a Permission Set assigned), JWT Bearer will fail with user hasn't approved this consumer because there is no browser screen to click "Allow."
  • 2. Server Clock Skew (exp Failure): Because the exp window is only a few minutes wide, if the external server's NTP system clock drifts by 4 minutes, Salesforce rejects the token as expired before it even arrives.
  • 3. Expired X.509 Certificate: Self-signed certificates typically expire after 1 or 2 years. Always put certificate expiration dates into your DevOps monitoring calendar 30 days ahead of expiry.
  • 4. Audience (aud) Mismatch After Sandbox Refresh: When a Partial or Full sandbox is refreshed from Production, your Connected App settings come over, but the valid aud claim changes from https://login.salesforce.com to https://test.salesforce.com.
Q2: Your enterprise InfoSec team mandates rotating all integration credentials every 90 days with zero downtime. How does that impact your choice of OAuth flow and outbound callout architecture?

A2: Designing for zero-downtime credential rotation upfront saves hundreds of hours of maintenance later:

  • Inbound Integrations (External System → Salesforce): Prefer External Client Apps or JWT Bearer Flow. With External Client Apps and modern Connected Apps, Salesforce supports staged secret and certificate rotation—you can upload a secondary certificate or stage a new Consumer Secret while the primary one remains active, switch the external middleware over to the new credential, verify traffic, and retire the old credential with zero downtime.
  • Outbound Integrations (Salesforce → External System): Never hardcode API keys, OAuth secrets, or endpoints inside Apex classes or Custom Metadata! Always use modern Named Credentials paired with External Credentials (Setup → Named Credentials). When it's time for quarterly rotation, an admin updates the secret or certificate in one centralized External Credential screen, and every Apex HttpRequest and Flow HTTP Callout picks it up automatically with zero code deployments.
Q3: What is the difference between a classic Connected App and the newer External Client App (ECA), and which one should architects choose in 2026? (Follow-Up Connection)
🎯 Say this first: "External Client Apps (ECAs) are Salesforce's modern, second-generation replacement for classic Connected Apps—built with strict separation between developer metadata and admin security policies."

A3: For over a decade, Connected Apps were the only way to configure OAuth 2.0 in Salesforce, but they suffered from messy packaging rules, global state dependencies, and clumsy secret rotation. External Client Apps (ECAs) solve this:

  • Clean Separation of Concerns: An ECA separates the developer's immutable OAuth structural definition from the local org admin's configurable security policies (like IP ranges, Permission Set assignments, and token timeouts).
  • Built for Second-Generation Packaging (2GP) & CI/CD: ECAs can be packaged cleanly inside Unlocked or Managed 2GP packages and deployed via Git without weird cross-org Consumer Key collisions.
  • Secure by Default: ECAs block deprecated insecure flows (like the Username-Password flow) out of the box and support native multi-key rotation. For any brand-new integration built in 2026, reach for an External Client App first.
Q4: If we enforce Single Sign-On (SSO) and Multi-Factor Authentication (MFA) across our org, how do we prevent getting locked out if our Identity Provider (Okta / Entra ID) goes down, and does MFA break our OAuth integrations? (Follow-Up Connection)

A4: This connects identity architecture directly to integration resilience:

  • The "Break-Glass" Admin Account: When you check "Disable login with Salesforce credentials" on your My Domain routing screen so all employees are forced through Okta or Microsoft Entra ID (where MFA is enforced), you must exclude one dedicated "Break-Glass" System Administrator account from Single Sign-On. Store that account's credentials and hardware security key inside a physical/enterprise password vault. If Okta suffers a global outage, your break-glass admin can still append ?login to your My Domain URL (https://yourcompany.my.salesforce.com/?login) to log in directly and temporarily restore access.
  • Why MFA Doesn't Break API Integrations: Salesforce's mandatory MFA enforcement applies to interactive UI logins. Headless integrations using the JWT Bearer Flow or Client Credentials Flow paired with the Salesforce Integration user license and Api Only permission are exempt from interactive MFA prompts because cryptographic certificates and pre-authorized machine tokens already provide strong multi-factor assurance.

Want to connect the dots across your Salesforce security, DevOps, and integration architecture? Check out these related deep-dives:

Was this article helpful?

Have a question or spotted a mistake? Ask here about this article

Found this guide useful? Buy me a coffee (opens in a new tab)

Comments

Questions, corrections and your own experience are all welcome.