
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:
Give the app your Gmail username and password
Trust they wouldn’t abuse it
Hope they stored it securely
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
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:
Navigate to App Manager
Click New Connected App
Fill in basic information
Check Enable OAuth Settings
Set Callback URL (your OAuth 2.0 redirect URI)
Select OAuth Scopes (start with
apiandrefresh_token)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
OAuthnpm 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:
Create a Connected App with OAuth 2.0 settings
Choose the appropriate OAuth 2.0 flow
Implement with security best practices
Test thoroughly with token refresh scenarios
If you have existing OAuth 1.0 code:
Audit your codebase immediately
Create migration plan
Implement OAuth 2.0 replacement
Test extensively
Deploy gradually
Remove old code completely
If you’re learning Salesforce integration:
Ignore all OAuth 1.0 content (it’s historical only)
Focus exclusively on OAuth 2.0
Practice with Web Server Flow first
Understand token lifecycle management
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.
