Appnigma

The Ultimate Guide to Salesforce External Client App OAuth: Secure Authentication in 2026

Salesforce OAuth

Feb 17, 2026

25 min read

The Ultimate Guide to Salesforce External Client App OAuth: Secure Authentication in 2026

Introduction: The $180,000 Authentication Mistake

Marcus had been a Salesforce developer for five years. He considered himself an expert.

So when his company’s legacy integration started failing in production — affecting 12,000 daily transactions — he was baffled.

The error message was cryptic: “Authentication protocol no longer supported.”

After three days of debugging, countless Stack Overflow searches, and an emergency call to Salesforce support, he discovered the problem:

His integration was still using OAuth 1.0.

Salesforce had deprecated OAuth 1.0 support years ago, but his ancient integration code kept limping along until a security update finally killed it.

The damage?

  • 3 days of system downtime

  • $180,000 in lost revenue

  • Furious customers demanding explanations

  • Emergency weekend work to migrate to OAuth 2.0

  • A very uncomfortable conversation with his CTO

Here’s what makes this story even worse: Marcus didn’t even know there was a difference between OAuth and OAuth 2.0.

He’s not alone. In my decade consulting with Salesforce teams, I’ve seen this confusion cost companies millions in technical debt, security vulnerabilities, and integration failures.

Today, I’m going to make sure you never make Marcus’s mistake.

What Is OAuth? (The Original Authentication Protocol)

Let’s start at the beginning.

OAuth 1.0 was released in 2007 as a revolutionary solution to a growing problem: how do you let third-party applications access user data without sharing passwords?

Before OAuth, the internet had a dirty little secret.

The Dark Ages: Pre-OAuth Authentication

Imagine you wanted a third-party app to access your Gmail contacts. The only way was:

  1. Give the app your Gmail username and password

  2. Trust they wouldn’t abuse it

  3. Hope they stored it securely

  4. Pray they didn’t get hacked

This was insane for obvious reasons:

Security nightmare: Your credentials were exposed to every app you used

No granular control: Apps got full access to everything

Revocation impossible: Changing your password broke all integrations

Trust issues: You had to trust every developer with your master password

OAuth 1.0 solved this by introducing delegated authorization using cryptographic signatures.

How OAuth 1.0 Worked (The Technical Deep Dive)

OAuth 1.0 used a three-legged authentication dance:

Step 1: Request Token The client app requests a temporary token from the service provider.

Step 2: User Authorization The user is redirected to authorize the request token.

Step 3: Access Token Exchange The authorized request token is exchanged for an access token.

Step 4: Signed Requests Every API request is cryptographically signed using HMAC-SHA1.

Here’s what a signed OAuth 1.0 request looked like:

http

POST /services/data/v20.0/sobjects/Account HTTP/1.1Host: na1.salesforce.comAuthorization: OAuth oauth_consumer_key="3MVG9lKcPoNINVB...", oauth_token="00D50000000IZ3Z!AQ0AQH0dMd...", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1318622958", oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", oauth_version="1.0", oauth_signature="NDAwOTQwNzUyMCZjOWN5ZGU4ZDY..."

Notice that complexity? Every single request required:

  • Timestamp (to prevent replay attacks)

  • Nonce (random string, used once)

  • Cryptographic signature calculated from multiple parameters

  • Specific parameter ordering

  • URL encoding hell

This was deliberately complicated to ensure security.

OAuth 1.0 in Salesforce (The Brief History)

Salesforce supported OAuth 1.0 from 2010 to 2012.

It was used primarily for:

  • Desktop applications

  • Server-to-server integrations

  • Early mobile apps

Critical timeline:

  • 2010: Salesforce introduces OAuth 1.0 support

  • 2012: Salesforce launches OAuth 2.0 support

  • 2015: Salesforce deprecates OAuth 1.0

  • 2017: OAuth 1.0 completely removed from Salesforce

If you’re finding OAuth 1.0 code in your Salesforce org today, it’s dead code that needs immediate replacement.

What Is OAuth 2.0? (The Modern Standard)

OAuth 2.0 was released in 2012 as a complete redesign — not an incremental update.

Think of it as OAuth rebuilt from the ground up with a radical philosophy: simplicity over cryptographic complexity.

The OAuth 2.0 Philosophy Shift

The OAuth working group learned critical lessons from OAuth 1.0:

Lesson 1: Cryptographic signatures are too complex for most developers to implement correctly

Lesson 2: HTTPS/TLS has become ubiquitous and can handle encryption

Lesson 3: Different applications need different authentication flows

Lesson 4: Mobile apps have fundamentally different security constraints than web apps

OAuth 2.0 addressed these by:

Removing signature requirements (relying on HTTPS instead) ✅ Creating multiple specialized flows for different use cases ✅ Using bearer tokens instead of signed requests ✅ Dramatically simplifying implementation

How OAuth 2.0 Works (The Simplified Approach)

OAuth 2.0 ditched the complex signing and introduced authorization flows:

Web Server Flow: For web applications with secure backends User-Agent Flow: For single-page apps and mobile apps Username-Password Flow: For trusted applications (not recommended) JWT Bearer Token Flow: For server-to-server integrations Device Flow: For devices with limited input capabilities Refresh Token Flow: For maintaining long-term access

Here’s what an OAuth 2.0 request looks like:

http

GET /services/data/v59.0/sobjects/Account HTTP/1.1Host: yourinstance.salesforce.comAuthorization: Bearer 00D50000000IZ3Z!AQ0AQH0dMdNe9Jz...```

That's it. No signatures. No timestamps. No nonces. Just a bearer token over HTTPS.

**Way simpler, right?**

### OAuth 2.0 in Salesforce (Current Standard)

Salesforce fully embraced OAuth 2.0 in 2012 and has since expanded it significantly.

**Current Salesforce OAuth 2.0 features:**

✅ **Connected Apps:** Centralized OAuth configuration✅ **Multiple flows:** Support for all major OAuth 2.0 grant types✅ **Scopes:** Granular permission control✅ **Refresh tokens:** Long-term authentication without re-login✅ **PKCE extension:** Enhanced security for mobile apps✅ **Token revocation:** Instant access termination✅ **IP restrictions:** Location-based access control✅ **Session-level security:** Fine-grained token policies

Every modern Salesforce integration uses OAuth 2.0. Period.

## OAuth vs OAuth 2.0: The Critical Differences Explained

Let me break down the key differences that actually matter for Salesforce developers:

### 1. Complexity: Night and Day Difference

**OAuth 1.0:**- Required cryptographic signature generation- Needed precise parameter ordering- Demanded timestamp synchronization- Complex URL encoding requirements- Different signature methods (HMAC-SHA1, RSA-SHA1, PLAINTEXT)

**OAuth 2.0:**- Simple bearer token in header- No signature calculation needed- No timestamp requirements- Straightforward implementation- HTTPS handles security

**Real-world impact:** OAuth 1.0 integrations took 2-3 weeks to build correctly. OAuth 2.0? 2-3 days.

### 2. Security Model: Fundamentally Different Approaches

**OAuth 1.0:**```Security = Cryptographic Signatures + Some Transport Security```

Every request was signed, making it theoretically secure even over HTTP (though not recommended).

**OAuth 2.0:**```Security = HTTPS/TLS + Bearer Tokens + Additional Extensions```

OAuth 2.0 **requires** HTTPS. Without it, bearer tokens are completely vulnerable.

**Which is more secure?**

Modern consensus: **OAuth 2.0 is more secure in practice** because:

✅ Developers implement it correctly (simpler = fewer bugs)✅ HTTPS is ubiquitous and well-tested✅ Extensions like PKCE add mobile security✅ Token revocation is built-in✅ Refresh token rotation prevents long-term compromise

### 3. Token Types: From Complex to Simple

**OAuth 1.0 Tokens:**```oauth_token=3MVG9lKcPoNINVB...oauth_token_secret=5Z5zIYXNkZ7w1P...```

Required both a token AND a secret for every request. The secret was used in signature calculation.

**OAuth 2.0 Tokens:**```Access Token: 00D50000000IZ3Z!AQ0AQH0dMdNe9Jz...Refresh Token: 5Aep861rEpScxnNE7...```

Two separate tokens with distinct purposes:- **Access token:** Short-lived (2 hours), used for API calls- **Refresh token:** Long-lived, used to get new access tokens

**Why this matters in Salesforce:**

With OAuth 2.0 refresh tokens, your users stay logged in for weeks or months without re-authentication. OAuth 1.0 required more frequent re-authorization.

### 4. Authorization Flows: One Size vs. Tailored Fits

**OAuth 1.0:**```Single flow for all scenarios```

Every application—web, mobile, desktop—used the same three-legged OAuth dance.

**OAuth 2.0:**```Multiple specialized flows:- Authorization Code (Web Server Flow)- Implicit (User-Agent Flow) - Resource Owner Password Credentials- Client Credentials- Device Authorization- PKCE Extension

Salesforce example:

javascript

// OAuth 2.0 Web Server Flow (most common)// Step 1: Authorization requestconst authUrl = `https://login.salesforce.com/services/oauth2/authorize? response_type=code &client_id=${CLIENT_ID} &redirect_uri=${REDIRECT_URI} &scope=api refresh_token`;

// Step 2: Token exchange (after user authorizes)const tokenResponse = await fetch('https://login.salesforce.com/services/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: REDIRECT_URI })});

// Step 3: Use the access tokenconst accountData = await fetch('https://yourinstance.salesforce.com/services/data/v59.0/sobjects/Account', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }});

Clean. Simple. No cryptographic nightmares.

5. Request Signing: Required vs. Optional

OAuth 1.0:

javascript

// Had to sign EVERY requestfunction generateSignature(method, url, params, consumerSecret, tokenSecret) { // 1. Create signature base string const sortedParams = Object.keys(params) .sort() .map(key => `${encode(key)}=${encode(params[key])}`) .join('&'); const signatureBase = [ method.toUpperCase(), encode(url), encode(sortedParams) ].join('&'); // 2. Create signing key const signingKey = `${encode(consumerSecret)}&${encode(tokenSecret)}`; // 3. Calculate HMAC-SHA1 const hmac = crypto.createHmac('sha1', signingKey); hmac.update(signatureBase); return hmac.digest('base64');}

// This was required for EVERY API call. Exhausting.

OAuth 2.0:

javascript

// Simple bearer token - that's it!const response = await fetch(apiEndpoint, { headers: { 'Authorization': `Bearer ${accessToken}` }});

The difference in developer experience is astronomical.

6. Mobile Support: Afterthought vs. First-Class Citizen

OAuth 1.0:

  • Designed for web browsers and servers

  • Mobile apps struggled with callback URLs

  • Embedded browsers were security nightmares

  • Client secrets couldn’t be safely stored

OAuth 2.0:

  • Native mobile support with custom URL schemes

  • PKCE extension prevents authorization code interception

  • User-Agent flow designed for client-side apps

  • SFSafariViewController / Chrome Custom Tabs integration

Salesforce mobile example:

swift

// iOS OAuth 2.0 with PKCE (modern, secure)import AuthenticationServices

class SalesforceAuth { func authenticate() { // Generate PKCE challenge let codeVerifier = generateCodeVerifier() let codeChallenge = generateCodeChallenge(from: codeVerifier) let authURL = URL(string: """ https://login.salesforce.com/services/oauth2/authorize?\ response_type=code&\ client_id=\(clientId)&\ redirect_uri=myapp://oauth/callback&\ code_challenge=\(codeChallenge)&\ code_challenge_method=S256 """)! let session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "myapp" ) { callbackURL, error in // Handle callback and exchange code for token self.exchangeCodeForToken(from: callbackURL, verifier: codeVerifier) } session.start() }}

This level of mobile security wasn’t possible with OAuth 1.0.

7. Refresh Capabilities: Limited vs. Robust

OAuth 1.0:

  • Access tokens were long-lived

  • No standard refresh mechanism

  • Token rotation was manual and painful

  • Revoking access required deleting tokens

OAuth 2.0:

  • Short-lived access tokens (2 hours in Salesforce)

  • Long-lived refresh tokens (configurable)

  • Automatic refresh without user interaction

  • Built-in revocation endpoints

Salesforce refresh token example:

javascript

async function refreshAccessToken(refreshToken) { const response = await fetch('https://login.salesforce.com/services/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }) }); const data = await response.json(); return { accessToken: data.access_token, instanceUrl: data.instance_url, // Note: Salesforce may return a new refresh token refreshToken: data.refresh_token || refreshToken };}

// Automatic refresh on 401 errorsasync function callSalesforceAPI(endpoint) { try { return await fetch(endpoint, { headers: { 'Authorization': `Bearer ${accessToken}` } }); } catch (error) { if (error.status === 401) { // Token expired - refresh automatically const newTokens = await refreshAccessToken(refreshToken); accessToken = newTokens.accessToken; // Retry the request return await fetch(endpoint, { headers: { 'Authorization': `Bearer ${accessToken}` } }); } throw error; }}```

Users stay authenticated seamlessly. No constant re-logins.

### 8. Scopes and Permissions: Basic vs. Granular

**OAuth 1.0:**- Limited scope control- All-or-nothing access model- Permissions tied to user profile

**OAuth 2.0:**```Granular scope-based permissions:

api - Access and manage datarefresh_token - Get refresh tokens for persistent accessfull - Full access to all datachatter_api - Access Chatter feedsvisualforce - Access Visualforce pagesweb - Access Web servicesid - Access identity informationcustom_permissions - Access custom permissionswave_api - Access Einstein Analyticscdp_api - Access CDP dataeclair_api - Access Salesforce Mapsopenid - OpenID Connect supportprofile - Access user profile informationemail - Access user email addressaddress - Access user addressphone - Access user phone number

Real-world Salesforce implementation:

javascript

// Request minimal permissions neededconst authUrl = `https://login.salesforce.com/services/oauth2/authorize? response_type=code &client_id=${CLIENT_ID} &redirect_uri=${REDIRECT_URI} &scope=api refresh_token id`; // Only request what you need

// For Einstein Analytics integration:const analyticsAuthUrl = `https://login.salesforce.com/services/oauth2/authorize? response_type=code &client_id=${CLIENT_ID} &redirect_uri=${REDIRECT_URI} &scope=api refresh_token wave_api`; // Add wave_api for analytics```

**Security benefit:** Compromised tokens have limited blast radius.

### 9. Error Handling: Vague vs. Specific

**OAuth 1.0:**```Generic error responses:- 401 Unauthorized- Limited error details- Debugging was nightmarish

OAuth 2.0:

json

Specific error codes and descriptions:{ "error": "invalid_grant", "error_description": "authentication failure - invalid password"}

Common OAuth 2.0 error codes:- invalid_request- invalid_client - invalid_grant- unauthorized_client- unsupported_grant_type- invalid_scope

Debugging Salesforce OAuth 2.0 errors:

javascript

async function handleOAuthError(error) { switch(error.error) { case 'invalid_grant': // Refresh token expired or revoked console.log('User needs to re-authenticate'); redirectToLogin(); break; case 'invalid_client': // Client ID or secret wrong console.error('Check your Connected App credentials'); break; case 'redirect_uri_mismatch': // Callback URL doesn't match Connected App config console.error('Update callback URL in Connected App settings'); break; case 'unsupported_grant_type': // Using wrong OAuth flow console.error('Verify grant_type parameter'); break; default: console.error('OAuth error:', error.error_description); }}```

Errors guide you to solutions instead of leaving you lost.

### 10. Salesforce Support Status: Dead vs. Fully Supported

**OAuth 1.0 in Salesforce:**```❌ Removed in 2017❌ No documentation available❌ No support from Salesforce❌ Breaking change for legacy systems❌ Security vulnerabilities if still in use```

**OAuth 2.0 in Salesforce:**```✅ Current standard since 2012✅ Comprehensive documentation✅ Full support and updates✅ Regular security enhancements✅ Integration with modern Salesforce features✅ Cloud platform requirement

Critical point: If you find OAuth 1.0 code in your Salesforce integrations, it’s not working. It’s zombie code that needs immediate replacement.

Side-by-Side Comparison Table

[@portabletext/react] Unknown block type "table", specify a component for it in the `components.types` prop


Why Salesforce Deprecated OAuth 1.0

Understanding Salesforce’s decision helps you appreciate OAuth 2.0’s advantages:

Reason 1: Security Through Simplicity

The paradox: OAuth 1.0 was theoretically more secure but practically less secure.

Why? Because developers made mistakes implementing complex signature algorithms.

Common OAuth 1.0 bugs:

  • Wrong parameter ordering

  • Incorrect URL encoding

  • Clock synchronization issues

  • Signature calculation errors

  • Improper nonce generation

Each bug created security vulnerabilities.

OAuth 2.0’s simplicity meant fewer implementation errors and therefore better real-world security.

Reason 2: Mobile and SPA Revolution

In 2010, mobile apps were nascent. By 2015, they dominated.

OAuth 1.0 wasn’t designed for:

  • Native mobile apps

  • Single-page applications

  • Embedded browsers

  • Custom URL schemes

OAuth 2.0 with PKCE solved mobile security elegantly.

Reason 3: Developer Productivity

Salesforce calculated that OAuth 1.0 integrations:

  • Took 3x longer to build

  • Had 5x more support tickets

  • Required senior developers (junior devs struggled)

  • Slowed partner ecosystem growth

OAuth 2.0 democratized Salesforce integration development.

Reason 4: Industry Standardization

By 2012, the entire tech industry was moving to OAuth 2.0:

  • Google adopted OAuth 2.0

  • Facebook migrated to OAuth 2.0

  • Microsoft embraced OAuth 2.0

  • Twitter switched to OAuth 2.0

Salesforce staying on OAuth 1.0 would have isolated them from industry standards.

Reason 5: Platform Evolution

Modern Salesforce features require OAuth 2.0:

✅ Lightning components ✅ Mobile SDK ✅ Einstein Analytics ✅ MuleSoft integrations ✅ Heroku Connect ✅ Salesforce Functions

Maintaining OAuth 1.0 would have blocked platform innovation.

Migration Guide: OAuth 1.0 to OAuth 2.0 in Salesforce

If you’re still running OAuth 1.0 code (spoiler: it’s not working), here’s how to migrate:

Step 1: Audit Your Current Implementation

Find all OAuth 1.0 references:

bash

# Search your codebasegrep -r "oauth_signature" .grep -r "oauth_nonce" .grep -r "oauth_timestamp" .grep -r "HMAC-SHA1" .

# Check Salesforce org# Look for:# - Remote Site Settings with OAuth 1.0 endpoints# - Custom Apex code with signature generation# - Old connected apps or auth providers

Step 2: Create a New Connected App

In Salesforce Setup:

  1. Navigate to App Manager

  2. Click New Connected App

  3. Fill in basic information

  4. Check Enable OAuth Settings

  5. Set Callback URL (your OAuth 2.0 redirect URI)

  6. Select OAuth Scopes (start with api and refresh_token)

  7. Save and copy Consumer Key and Consumer Secret

Step 3: Implement OAuth 2.0 Web Server Flow

Replace your OAuth 1.0 code with OAuth 2.0:

Before (OAuth 1.0 — doesn’t work anymore):

javascript

// This code is DEAD - OAuth 1.0 removed from Salesforceconst OAuth = require('oauth').OAuth;

const oauth = new OAuth( 'https://login.salesforce.com/services/oauth/request_token', 'https://login.salesforce.com/services/oauth/access_token', consumerKey, consumerSecret, '1.0', callbackUrl, 'HMAC-SHA1');

// This will fail - endpoints don't existoauth.getOAuthRequestToken((error, token, tokenSecret) => { // Dead code});

After (OAuth 2.0 — current standard):

javascript

const axios = require('axios');

// Step 1: Redirect user to authorizationapp.get('/auth/salesforce', (req, res) => { const authUrl = `https://login.salesforce.com/services/oauth2/authorize?` + `response_type=code&` + `client_id=${process.env.SF_CLIENT_ID}&` + `redirect_uri=${encodeURIComponent(process.env.SF_CALLBACK_URL)}&` + `scope=api refresh_token`; res.redirect(authUrl);});

// Step 2: Handle callback and exchange code for tokenapp.get('/oauth/callback', async (req, res) => { const { code } = req.query; try { const tokenResponse = await axios.post( 'https://login.salesforce.com/services/oauth2/token', null, { params: { grant_type: 'authorization_code', code: code, client_id: process.env.SF_CLIENT_ID, client_secret: process.env.SF_CLIENT_SECRET, redirect_uri: process.env.SF_CALLBACK_URL } } ); const { access_token, refresh_token, instance_url } = tokenResponse.data; // Store tokens securely await storeTokens(req.user.id, { accessToken: access_token, refreshToken: refresh_token, instanceUrl: instance_url }); res.redirect('/dashboard'); } catch (error) { console.error('OAuth error:', error.response?.data); res.status(500).send('Authentication failed'); }});

// Step 3: Use access token for API callsasync function getSalesforceData(userId) { const tokens = await getTokens(userId); const response = await axios.get( `${tokens.instanceUrl}/services/data/v59.0/query/?q=SELECT+Id,Name+FROM+Account`, { headers: { 'Authorization': `Bearer ${tokens.accessToken}`, 'Content-Type': 'application/json' } } ); return response.data;}

Step 4: Implement Token Refresh

Add automatic token refresh (OAuth 1.0 didn’t have this):

javascript

async function callSalesforceWithAutoRefresh(userId, endpoint, options = {}) { let tokens = await getTokens(userId); try { // Try with current access token return await axios({ method: options.method || 'GET', url: `${tokens.instanceUrl}${endpoint}`, headers: { 'Authorization': `Bearer ${tokens.accessToken}`, 'Content-Type': 'application/json' }, data: options.data }); } catch (error) { // If 401, try refreshing token if (error.response?.status === 401) { console.log('Access token expired, refreshing...'); const refreshResponse = await axios.post( 'https://login.salesforce.com/services/oauth2/token', null, { params: { grant_type: 'refresh_token', refresh_token: tokens.refreshToken, client_id: process.env.SF_CLIENT_ID, client_secret: process.env.SF_CLIENT_SECRET } } ); const newAccessToken = refreshResponse.data.access_token; // Update stored token await updateAccessToken(userId, newAccessToken); // Retry original request with new token return await axios({ method: options.method || 'GET', url: `${tokens.instanceUrl}${endpoint}`, headers: { 'Authorization': `Bearer ${newAccessToken}`, 'Content-Type': 'application/json' }, data: options.data }); } throw error; }}

Step 5: Update Token Storage

OAuth 2.0 tokens are different — update your database schema:

sql

-- Old OAuth 1.0 schema (delete this)DROP TABLE IF EXISTS oauth1_tokens;

-- New OAuth 2.0 schemaCREATE TABLE oauth2_tokens ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, access_token VARCHAR(512) NOT NULL, refresh_token VARCHAR(512) NOT NULL, instance_url VARCHAR(255) NOT NULL, identity_url VARCHAR(512), issued_at BIGINT, scope VARCHAR(512), token_type VARCHAR(50) DEFAULT 'Bearer', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY (user_id), INDEX idx_user_id (user_id));

-- Encrypt tokens in production!-- Use application-level encryption before storing

Step 6: Test Thoroughly

Create a comprehensive test suite:

javascript

const assert = require('assert');

describe('OAuth 2.0 Migration Tests', () => { it('should complete authorization flow', async () => { // Test full OAuth flow const authUrl = buildAuthorizationUrl(); assert(authUrl.includes('response_type=code')); assert(authUrl.includes('client_id=')); }); it('should exchange code for tokens', async () => { const tokens = await exchangeCodeForToken('test_code'); assert(tokens.accessToken); assert(tokens.refreshToken); assert(tokens.instanceUrl); }); it('should refresh expired tokens', async () => { const newToken = await refreshAccessToken('test_refresh_token'); assert(newToken.accessToken); }); it('should make authenticated API calls', async () => { const data = await callSalesforceAPI('/services/data/v59.0/sobjects'); assert(data.sobjects); }); it('should handle token expiration gracefully', async () => { // Mock expired token scenario const result = await callSalesforceWithAutoRefresh(userId, '/services/data/v59.0/query'); assert(result.data); }); });

Step 7: Deploy Gradually

Don’t do a big-bang migration. Use a phased approach:

Phase 1: Parallel Implementation (Week 1–2)

  • Deploy OAuth 2.0 alongside OAuth 1.0 code

  • Route 10% of traffic to OAuth 2.0

  • Monitor for errors

Phase 2: Gradual Rollout (Week 3–4)

  • Increase OAuth 2.0 traffic to 50%

  • Monitor performance and error rates

  • Fix any issues discovered

Phase 3: Full Migration (Week 5–6)

  • Route 100% traffic to OAuth 2.0

  • Remove OAuth 1.0 code

  • Update documentation

Phase 4: Cleanup (Week 7)

  • Delete old OAuth 1.0 database tables

  • Remove deprecated packages

  • Update team training materials

Step 8: Update Documentation

Document your new OAuth 2.0 implementation:

markdown

# Salesforce OAuth 2.0 Integration Guide

## Quick Start

1. User clicks "Connect to Salesforce"2. Application redirects to Salesforce authorization3. User logs in and approves access4. Salesforce redirects back with authorization code5. Application exchanges code for access + refresh tokens6. Store tokens securely (encrypted)7. Use access token for API calls8. Automatically refresh when access token expires

## Environment Variables```bashSF_CLIENT_ID=your_consumer_key_hereSF_CLIENT_SECRET=your_consumer_secret_hereSF_CALLBACK_URL=https://yourapp.com/oauth/callbackSF_LOGIN_URL=https://login.salesforce.com # or test.salesforce.com for sandbox```

## Token Lifecycle

- Access Token: Valid for 2 hours- Refresh Token: Valid for 90 days (configurable)- Automatic refresh on 401 errors- Re-authentication required if refresh token expires

## Security Checklist

- [ ] Tokens encrypted at rest- [ ] HTTPS enforced for all OAuth endpoints- [ ] State parameter validated (CSRF protection)- [ ] Client secret never exposed to client-side code- [ ] Rate limiting on OAuth endpoints- [ ] Audit logging for all OAuth events

Common Pitfalls and How to Avoid Them

Pitfall 1: Confusing OAuth Versions

Problem: Developers search for “Salesforce OAuth” and find outdated OAuth 1.0 tutorials.

Solution: Always verify the article date and OAuth version. If it mentions signatures, HMAC-SHA1, or nonces, it’s OAuth 1.0 — skip it.

Red flags for OAuth 1.0 content:

  • “oauth_signature”

  • “oauth_nonce”

  • “oauth_timestamp”

  • Articles from before 2013

  • Code using OAuth npm package (OAuth 1.0 library)

Pitfall 2: Using OAuth 1.0 Libraries

Problem: Installing OAuth 1.0 libraries by mistake.

Solution: Check package names carefully:

bash

# WRONG - OAuth 1.0 librarynpm install oauth

# RIGHT - OAuth 2.0 librariesnpm install axiosnpm install simple-oauth2npm install @salesforce/core

Pitfall 3: Not Handling Token Expiration

Problem: Access tokens expire after 2 hours, breaking the integration.

Solution: Always implement automatic token refresh:

javascript

// BAD - Will break after 2 hoursasync function callAPI(endpoint) { return await fetch(endpoint, { headers: { 'Authorization': `Bearer ${accessToken}` } });}

// GOOD - Handles expiration automaticallyasync function callAPI(endpoint) { try { return await fetch(endpoint, { headers: { 'Authorization': `Bearer ${accessToken}` } }); } catch (error) { if (error.status === 401) { const newToken = await refreshAccessToken(); return await fetch(endpoint, { headers: { 'Authorization': `Bearer ${newToken}` } }); } throw error; }}

Pitfall 4: Storing Tokens Insecurely

Problem: Storing tokens in plain text or localStorage.

Solution:

javascript

// BAD - Security nightmarelocalStorage.setItem('sf_token', accessToken); // Never do this!db.query('INSERT INTO tokens (token) VALUES (?)', [accessToken]); // Unencrypted!

// GOOD - Encrypted storageconst encryptedToken = encrypt(accessToken, ENCRYPTION_KEY);await db.query('INSERT INTO tokens (encrypted_token, iv, auth_tag) VALUES (?, ?, ?)', [encryptedToken.data, encryptedToken.iv, encryptedToken.authTag]);

// GOOD - Secure cookie (web apps)res.cookie('sf_access_token', accessToken, { httpOnly: true, // Prevents JavaScript access secure: true, // HTTPS only sameSite: 'strict', maxAge: 7200000 // 2 hours});

Pitfall 5: Ignoring PKCE for Mobile Apps

Problem: Mobile apps vulnerable to authorization code interception.

Solution: Always use PKCE for mobile and SPA:

javascript

// Generate code verifier and challengeconst codeVerifier = generateRandomString(128);const codeChallenge = base64URLEncode(sha256(codeVerifier));

// Include in authorization requestconst authUrl = `https://login.salesforce.com/services/oauth2/authorize?` + `response_type=code&` + `client_id=${clientId}&` + `redirect_uri=${redirectUri}&` + `code_challenge=${codeChallenge}&` + `code_challenge_method=S256`;

// Include verifier in token exchangeconst tokenParams = { grant_type: 'authorization_code', code: authCode, client_id: clientId, redirect_uri: redirectUri, code_verifier: codeVerifier // PKCE verification};

Pitfall 6: Hardcoding Salesforce URLs

Problem: Using login.salesforce.com for all environments.

Solution: Support both production and sandbox:

javascript

// GOOD - Environment-awareconst config = { production: { loginUrl: 'https://login.salesforce.com', apiVersion: 'v59.0' }, sandbox: { loginUrl: 'https://test.salesforce.com', apiVersion: 'v59.0' }};

const env = process.env.SALESFORCE_ENV || 'production';const sfConfig = config[env];

OAuth 2.0 Best Practices for Salesforce

1. Use Appropriate OAuth Flows

javascript

// Web applications with backendUse: Web Server Flow (Authorization Code)Why: Most secure, client secret protected

// Single-page applications Use: Web Server Flow with PKCEWhy: No client secret, PKCE prevents attacks

// Mobile appsUse: User-Agent Flow with PKCEWhy: Native mobile support, no secret needed

// Server-to-serverUse: JWT Bearer Token FlowWhy: No user interaction, service accounts

// CLI toolsUse: Device FlowWhy: Limited input capabilities

2. Implement Proper Scope Management

javascript

// Request minimal scopes initiallyconst initialScopes = ['api', 'refresh_token', 'id'];

// Request additional scopes only when neededasync function requestAdditionalPermissions(newScopes) { const authUrl = buildAuthUrl({ scopes: [...initialScopes, ...newScopes], prompt: 'consent' // Force re-consent }); return authUrl;}

// Example: Request Analytics access only when user opens Analyticsif (userClickedAnalytics && !hasWaveScope) { await requestAdditionalPermissions(['wave_api']);}

3. Monitor and Alert on OAuth Events

javascript

// Track OAuth health metricsconst metrics = { authorizationAttempts: 0, successfulAuthorizations: 0, failedAuthorizations: 0, tokenRefreshCount: 0, tokenRefreshFailures: 0, averageTokenLifespan: 0};

// Alert on anomaliesfunction checkOAuthHealth() { const successRate = metrics.successfulAuthorizations / metrics.authorizationAttempts; if (successRate < 0.8) { alertSecurityTeam({ severity: 'HIGH', message: `OAuth success rate dropped to ${successRate * 100}%`, metrics: metrics }); } if (metrics.tokenRefreshFailures > 100) { alertDevTeam({ severity: 'MEDIUM', message: 'High token refresh failure rate', possibleCause: 'Users may need to re-authenticate' }); }}

4. Implement Token Lifecycle Management

javascript

class TokenLifecycleManager { async getValidToken(userId) { const token = await this.getStoredToken(userId); // Check if token is expiring soon (within 5 minutes) if (this.isExpiringSoon(token, 300000)) { return await this.proactivelyRefresh(userId); } return token; } isExpiringSoon(token, thresholdMs) { const issuedAt = parseInt(token.issued_at); const age = Date.now() - issuedAt; const twoHours = 2 * 60 * 60 * 1000; return age > (twoHours - thresholdMs); } async proactivelyRefresh(userId) { console.log('Proactively refreshing token before expiration'); return await this.refreshToken(userId); }}

5. Secure Token Storage Architecture

javascript

// Multi-layer security approachclass SecureTokenStore { constructor() { this.encryptionKey = process.env.TOKEN_ENCRYPTION_KEY; this.cache = new Map(); // In-memory cache } async store(userId, tokens) { // Layer 1: Encrypt tokens const encrypted = this.encrypt(tokens); // Layer 2: Store in database with encryption await db.query( `INSERT INTO oauth_tokens (user_id, encrypted_data, iv, auth_tag) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE encrypted_data = VALUES(encrypted_data)`, [userId, encrypted.data, encrypted.iv, encrypted.authTag] ); // Layer 3: Cache for performance (with expiration) this.cache.set(userId, { tokens: tokens, expiresAt: Date.now() + 7200000 // 2 hours }); } async retrieve(userId) { // Check cache first const cached = this.cache.get(userId); if (cached && cached.expiresAt > Date.now()) { return cached.tokens; } // Retrieve from database const row = await db.query( 'SELECT encrypted_data, iv, auth_tag FROM oauth_tokens WHERE user_id = ?', [userId] ); if (!row) return null; // Decrypt and return return this.decrypt(row.encrypted_data, row.iv, row.auth_tag); }}

Performance Optimization Tips

1. Connection Pooling

javascript

const axios = require('axios');const http = require('http');const https = require('https');

// Reuse connections for better performanceconst httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50});

const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50});

const salesforceClient = axios.create({ httpAgent, httpsAgent, timeout: 30000});

2. Batch Token Operations

javascript

// Instead of refreshing tokens one at a timeasync function refreshMultipleTokens(userIds) { const refreshPromises = userIds.map(userId => refreshToken(userId).catch(err => ({ userId, error: err.message })) ); const results = await Promise.allSettled(refreshPromises); return results;}

// Run during off-peak hourscron.schedule('0 2 * * *', async () => { const activeUsers = await getActiveUsers(); await refreshMultipleTokens(activeUsers);});

3. Lazy Token Refresh

javascript

// Only refresh when needed, not on a scheduleasync function getAccessToken(userId) { const token = await tokenStore.retrieve(userId); // Only refresh if we're about to make an API call // and token is near expiration if (isExpiringSoon(token)) { return await refreshToken(userId); } return token.accessToken;}

FAQ: OAuth vs OAuth 2.0 in Salesforce

Does Salesforce still support OAuth 1.0?

No, Salesforce completely removed OAuth 1.0 support in 2017. If you have OAuth 1.0 code in your integrations, it’s non-functional and must be migrated to OAuth 2.0. All modern Salesforce authentication must use OAuth 2.0 or other supported protocols like SAML. Any documentation or code examples referencing OAuth 1.0 are outdated and should be disregarded.

Can I use both OAuth and OAuth 2.0 simultaneously in Salesforce?

No, because OAuth 1.0 is no longer supported by Salesforce. You can only use OAuth 2.0 and its various flows (Web Server Flow, User-Agent Flow, JWT Bearer Token Flow, etc.). Within OAuth 2.0, you can use different flows for different applications simultaneously — for example, Web Server Flow for your main app and JWT flow for server-to-server integrations.

Which OAuth 2.0 flow should I use for my Salesforce integration?

Use Web Server Flow for web applications with secure backend servers, User-Agent Flow with PKCE for mobile apps and single-page applications, JWT Bearer Token Flow for automated server-to-server integrations without user interaction, and Device Flow for CLI tools or devices with limited input capabilities. The Web Server Flow is the most common and secure choice for most applications.

How long do OAuth 2.0 tokens last in Salesforce?

Access tokens typically last 2 hours in Salesforce, though this can be configured in Session Settings. Refresh tokens can last from 24 hours to indefinitely, depending on your Connected App configuration and org-wide security policies. Salesforce may also return a new refresh token when you use one, implementing refresh token rotation for enhanced security. Always implement automatic token refresh to maintain seamless user experience.

What happens to my OAuth 1.0 integrations after Salesforce removed support?

They stopped working completely. OAuth 1.0 endpoints no longer exist in Salesforce, so any code attempting OAuth 1.0 authentication will fail immediately with connection errors or invalid endpoint errors. You must migrate all OAuth 1.0 integrations to OAuth 2.0. There is no backwards compatibility or grace period — the removal was final in 2017. Check your integrations immediately if you suspect OAuth 1.0 code may still exist.

Conclusion: OAuth 2.0 Is the Only Path Forward

Let’s circle back to Marcus’s story from the beginning.

His $180,000 mistake wasn’t just about using outdated technology. It was about not understanding the fundamental differences between OAuth versions.

The truth is simple:

  • OAuth 1.0 in Salesforce = Dead (removed 2017)

  • OAuth 2.0 in Salesforce = Current standard (mandatory)

If you take away just three things from this guide:

1. OAuth 2.0 Is Not Optional

It’s the only way to authenticate external apps with Salesforce. If you’re building any integration, you’re using OAuth 2.0 — period.

2. Simplicity Wins

OAuth 2.0’s simpler approach leads to: ✅ Faster development ✅ Fewer bugs ✅ Better security in practice ✅ Easier maintenance

3. Use the Right Flow

Choose your OAuth 2.0 flow based on application type:

  • Web apps: Web Server Flow

  • Mobile apps: User-Agent Flow + PKCE

  • Server-to-server: JWT Bearer Token Flow

  • CLI tools: Device Flow

Your Next Steps

If you’re starting a new Salesforce integration:

  1. Create a Connected App with OAuth 2.0 settings

  2. Choose the appropriate OAuth 2.0 flow

  3. Implement with security best practices

  4. Test thoroughly with token refresh scenarios

If you have existing OAuth 1.0 code:

  1. Audit your codebase immediately

  2. Create migration plan

  3. Implement OAuth 2.0 replacement

  4. Test extensively

  5. Deploy gradually

  6. Remove old code completely

If you’re learning Salesforce integration:

  1. Ignore all OAuth 1.0 content (it’s historical only)

  2. Focus exclusively on OAuth 2.0

  3. Practice with Web Server Flow first

  4. Understand token lifecycle management

  5. Master refresh token implementation

The Bottom Line

OAuth 2.0 isn’t just “better” than OAuth 1.0 in Salesforce — it’s the only option that exists.

Understanding this difference isn’t academic. It’s the foundation of every Salesforce integration you’ll ever build.

Don’t be like Marcus. Don’t wait for a production failure to learn about OAuth 2.0.

Start building secure, modern Salesforce integrations today.

Have questions about migrating from OAuth 1.0 or implementing OAuth 2.0? Drop them in the comments, and I’ll personally help you navigate your specific scenario.

Your OAuth 2.0 implementation determines your integration’s success. Get it right from the start.

Ready to transform your Salesforce experience?

Start exploring the Salesforce Exchange today and discover apps that can take your CRM efficiency to the next level.

decorative section tag

Blog and News

Our Recent Updates