- 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.
• 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.
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.
🔑 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)PKCEon all Web Server / Authorization Code flows to prevent authorization code interception attacks. - Intersection of Authority: Giving a Connected App or External Client App the
fullorapiscope 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 itsclient_secretandcode_verifier) server-to-server for anaccess_tokenandrefresh_token. - User-Agent Flow (Legacy Implicit Flow): The old browser-based flow that returned an
access_tokendirectly 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 privateRSA SHA256key, and sends it to Salesforce. Salesforce verifies the signature against the uploadedX.509certificate and returns anaccess_tokenimmediately. 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_idandclient_secretdirectly for anaccess_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 therefresh_token/offline_accessscope 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..."
aud (Audience) Mismatch & Token Scope MythTrap 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.
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
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.509private 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 cryptographiccode_verifierandcode_challengefor 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 Integrationlicense. 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 thesubclaim. - (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_secretwhile 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.
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.
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:
- The external server holds the private key, and the Salesforce Connected App / External Client App holds the matching public
X.509certificate. - The app's OAuth policy is explicitly set to Admin approved users are pre-authorized.
- 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.
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(oroffline_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 viaSetup → Connected Apps OAuth Usageis the fastest way to cut off a compromised external app without deactivating human users.
🔄 Follow-Up Scenarios (Connecting the Dots)
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 consumerbecause there is no browser screen to click "Allow." - 2. Server Clock Skew (
expFailure): Because theexpwindow 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.509Certificate: 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 validaudclaim changes fromhttps://login.salesforce.comtohttps://test.salesforce.com.
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 ApexHttpRequestand Flow HTTP Callout picks it up automatically with zero code deployments.
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 Managed2GPpackages 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.
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
?loginto 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 Integrationuser license andApi Onlypermission 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:
- Zero-Code Integration Security: Mastering Salesforce Named Credentials and External Credentials
- External Client Apps (ECA) vs. Connected Apps: The Modern Guide to Salesforce OAuth Packaging
- Automating Headless Salesforce CI/CD Pipelines with JWT Bearer Authentication and GitHub Actions
- Principle of Least Privilege: Configuring the 5 Free Salesforce Integration User Licenses
- Salesforce Single Sign-On (SSO) Architecture: SAML 2.0 vs. OpenID Connect (OIDC) & Break-Glass Admins
Comments
Questions, corrections and your own experience are all welcome.
Post a Comment