# OAuth Flows Reference for Connected Apps
Detailed OAuth flow patterns, configuration examples, and implementation guidance for the integration-connectivity-connected-app-configure skill.
---
## Flow Selection Decision Tree
```text
START
│
├─ Do you have a backend server?
│ ├─ YES: Can it securely store secrets?
│ │ ├─ YES: Authorization Code Flow (Web Server)
│ │ └─ NO: Authorization Code + PKCE (SPA/Mobile)
│ │
│ └─ NO: Is this server-to-server?
│ ├─ YES: JWT Bearer Flow
│ └─ NO: Device Authorization Flow (CLI/IoT)
│
└─ Is this for a specific integration user?
├─ YES: JWT Bearer Flow
└─ NO: Authorization Code Flow
```
---
## Authorization Code Flow (Web Server)
### When to Use
- Web applications with backend server
- Can securely store consumer secret
- User-interactive flow needed
- Examples: Portal, Integration Hub, Admin Console
### Connected App Configuration
**Minimal scopes for API access**:
```xml
https://app.example.com/oauth/callback
Api
RefreshToken
false
```
**With OpenID Connect**:
```xml
https://app.example.com/oauth/callback
Api
RefreshToken
OpenID
true
false
```
### Security Checklist
- [ ] HTTPS callback URL (no localhost in production)
- [ ] Consumer secret stored in environment variables (never in code)
- [ ] State parameter validated (CSRF protection)
- [ ] Authorization code used only once
- [ ] Refresh token rotation enabled
- [ ] IP restrictions configured (optional)
### Common Issues
**Problem**: "redirect_uri_mismatch" error
- **Cause**: Callback URL doesn't match exactly
- **Fix**: Ensure exact match including protocol, domain, path, and query parameters
**Problem**: "invalid_client_id" error
- **Cause**: Consumer key incorrect or app not deployed
- **Fix**: Verify consumer key from Setup > App Manager
---
## Authorization Code + PKCE (Public Clients)
### When to Use
- Single Page Applications (React, Vue, Angular)
- Mobile apps (iOS, Android)
- Desktop apps
- Any client that cannot securely store secrets
### Connected App Configuration
```xml
myapp://oauth/callback
Api
RefreshToken
true
true
infinite
true
```
### External Client App Configuration
```xml
myapp://oauth/callback
MyMobileApp
true
true
false
```
### Implementation Pattern (JavaScript)
```javascript
// Generate PKCE verifier and challenge
function generatePKCE() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = base64URLEncode(array);
return crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
.then(buffer => ({
codeVerifier,
codeChallenge: base64URLEncode(new Uint8Array(buffer))
}));
}
// Store verifier in sessionStorage (cleared on close)
const { codeVerifier, codeChallenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', codeVerifier);
// Authorization URL
const authUrl = `https://login.salesforce.com/services/oauth2/authorize?` +
`response_type=code` +
`&client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&scope=api%20refresh_token` +
`&state=${STATE}` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256`;
```
### Security Checklist
- [ ] PKCE required in Connected App config
- [ ] Consumer secret optional
- [ ] Refresh token rotation enabled
- [ ] Code verifier stored securely (sessionStorage, keychain)
- [ ] State parameter validated
- [ ] Deep link callback handled securely (mobile)
---
## JWT Bearer Flow (Server-to-Server)
### When to Use
- CI/CD pipelines (GitHub Actions, Jenkins)
- Backend integrations without user interaction
- Service accounts
- Scheduled jobs
### Prerequisites
**1. Generate X.509 Certificate**:
```bash
# Generate private key
openssl genrsa -out server.key 2048
# Generate certificate signing request
openssl req -new -key server.key -out server.csr
# Self-sign certificate (valid 1 year)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
```
**2. Upload to Salesforce**:
- Setup > Certificate and Key Management > Create Self-Signed Certificate
- Or use the certificate from step 1
### Connected App Configuration
```xml
JWTAuthCertificate
AUTO_GENERATED
Api
Web
true
ENFORCE
```
**Important**: No `callbackUrl` needed for JWT Bearer flow.
### Pre-Authorization
**Option 1: Permission Set**
```xml
MyJWTApp
true
false
```
Assign to integration user: Setup > Users > [User] > Permission Set Assignments
**Option 2: Profile**
Setup > Manage Connected Apps > [App] > Edit Policies > Permitted Users = "Admin approved users are pre-authorized"
### Implementation Pattern (Node.js)
```javascript
const jwt = require('jsonwebtoken');
const axios = require('axios');
const fs = require('fs');
async function getAccessToken() {
const privateKey = fs.readFileSync('server.key', 'utf8');
const claims = {
iss: process.env.CONSUMER_KEY,
sub: 'integration@company.com', // Pre-authorized user
aud: 'https://login.salesforce.com',
exp: Math.floor(Date.now() / 1000) + 300 // 5 min
};
const assertion = jwt.sign(claims, privateKey, { algorithm: 'RS256' });
const response = await axios.post('https://login.salesforce.com/services/oauth2/token',
new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: assertion
})
);
return response.data.access_token;
}
```
### Security Checklist
- [ ] Private key stored securely (secrets manager, not in repo)
- [ ] Certificate uploaded to Salesforce
- [ ] User pre-authorized via Permission Set
- [ ] IP restrictions configured
- [ ] Token expiration set (exp claim)
- [ ] Audience (aud) set correctly (login vs test.salesforce.com)
### Common Issues
**Problem**: "user hasn't approved this consumer" error
- **Cause**: User not pre-authorized
- **Fix**: Assign Permission Set or configure admin pre-approval
**Problem**: "invalid_grant" error
- **Cause**: Certificate mismatch or expired token
- **Fix**: Verify certificate name matches `` tag, check exp claim
---
## Device Authorization Flow
### When to Use
- CLI tools (sf CLI, custom CLIs)
- Smart TVs, Set-top boxes
- IoT devices without keyboard
- Any device with limited input capability
### Connected App Configuration
```xml
http://localhost:8080
Api
RefreshToken
true
```
### Implementation Pattern (Python)
```python
import requests
import time
CLIENT_ID = 'your_consumer_key'
DEVICE_CODE_URL = 'https://login.salesforce.com/services/oauth2/device/code'
TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token'
# Step 1: Request device code
response = requests.post(DEVICE_CODE_URL, data={
'client_id': CLIENT_ID,
'scope': 'api refresh_token'
})
data = response.json()
# Step 2: Display user code
print(f"Visit: {data['verification_uri']}")
print(f"Enter code: {data['user_code']}")
# Step 3: Poll for token
device_code = data['device_code']
interval = data['interval'] # Polling interval in seconds
while True:
time.sleep(interval)
token_response = requests.post(TOKEN_URL, data={
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'client_id': CLIENT_ID,
'code': device_code
})
if token_response.status_code == 200:
tokens = token_response.json()
print(f"Access Token: {tokens['access_token']}")
break
elif token_response.json().get('error') == 'authorization_pending':
continue # User hasn't authorized yet
else:
print(f"Error: {token_response.json()}")
break
```
### Security Checklist
- [ ] Consumer secret optional
- [ ] Polling interval respected (don't spam)
- [ ] Device code expires after timeout
- [ ] Refresh token stored securely
---
## Client Credentials Flow (ECA Only)
### When to Use
- Service accounts (not tied to specific user)
- Background processes
- Microservices
- Requires External Client App (not available in Connected Apps)
### External Client App Configuration
```xml
Api
MyServiceApp
```
**Source-control note:** if you also need to manage client-credentials security settings in source, retrieve `ExtlClntAppOauthSecuritySettings:MyServiceApp` from an org first and commit the retrieved `.ecaOauthSecurity-meta.xml` file as your starting point.
### Implementation Pattern
```bash
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=client_credentials" \
-d "client_id=" \
-d "client_secret="
```
### Security Checklist
- [ ] Execution user configured
- [ ] Consumer secret rotated regularly
- [ ] Scopes minimal (least privilege)
- [ ] IP restrictions enabled
---
## Refresh Token Patterns
### Standard Refresh
```javascript
async function refreshAccessToken(refreshToken) {
const response = await axios.post('https://login.salesforce.com/services/oauth2/token',
new URLSearchParams({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken
})
);
return response.data;
}
```
### With Token Rotation (Recommended)
When `isRefreshTokenRotationEnabled=true`, each refresh returns a NEW refresh token:
```javascript
async function refreshWithRotation(refreshToken) {
const response = await refreshAccessToken(refreshToken);
// Store NEW refresh token (old one is now invalid)
await secureStorage.set('refresh_token', response.refresh_token);
await secureStorage.set('access_token', response.access_token);
return response;
}
```
### Refresh Token Policies
| Policy | Description | Use Case |
|--------|-------------|----------|
| `infinite` | Never expires | Trusted integrations |
| `immediately` | Expires on use | Maximum security |
| `zero` | Not issued | Access token only |
**Configuration**:
```xml
infinite
true
```
---
## Named Credentials Integration
### Why Use Named Credentials
- Secrets managed by Salesforce (not in code)
- Automatic token refresh
- Per-user or per-org authentication
- Audit trail in Setup Audit Trail
### Create Named Credential for JWT Flow
```xml
https://yourinstance.salesforce.com
NoAuthentication
NamedUser
JWTAuthCertificate
YOUR_CONSUMER_KEY
JwtBearer
integration@company.com
```
### Use in Apex
```apex
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:SalesforceAPI/services/data/v66.0/query?q=SELECT+Id+FROM+Account');
req.setMethod('GET');
Http http = new Http();
HttpResponse res = http.send(req);
```
---
## Token Introspection
**Use**: Validate token status and metadata
```bash
curl -X POST https://login.salesforce.com/services/oauth2/introspect \
-d "token=" \
-d "client_id=" \
-d "client_secret=" \
-d "token_type_hint=access_token"
```
---
## Token Revocation
**Use**: Invalidate tokens on logout
```bash
curl -X POST https://login.salesforce.com/services/oauth2/revoke \
-d "token="
```
---
## Error Handling Patterns
### OAuth Error Response Structure
```json
{
"error": "invalid_grant",
"error_description": "authentication failure"
}
```
### Common Errors
| Error Code | Meaning | Resolution |
|------------|---------|------------|
| `invalid_client_id` | Consumer key invalid | Verify key from Setup |
| `invalid_client` | Secret incorrect | Check consumer secret |
| `redirect_uri_mismatch` | Callback URL mismatch | Match exactly with config |
| `invalid_grant` | Auth code expired/used | Request new authorization |
| `unsupported_grant_type` | Flow not enabled | Enable in Connected App |
| `invalid_scope` | Scope not allowed | Check available scopes |
| `access_denied` | User declined | User must approve |
### Retry Logic Example
```javascript
async function callSalesforceAPI(accessToken, retries = 1) {
try {
return await axios.get('https://instance.salesforce.com/services/data/v66.0/query', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
} catch (error) {
if (error.response?.status === 401 && retries > 0) {
// Token expired, refresh and retry
const newToken = await refreshAccessToken();
return callSalesforceAPI(newToken, retries - 1);
}
throw error;
}
}
```
---
## Scoring Impact by Flow
| Flow | Security Score Impact | Best Practices Score |
|------|----------------------|---------------------|
| Authorization Code + PKCE | +10 (PKCE enabled) | +10 (modern flow) |
| JWT Bearer | +5 (certificate) | +15 (server-to-server best practice) |
| Device Authorization | +5 (secret optional) | +10 (appropriate for CLI) |
| Username-Password | -10 (deprecated) | -10 (anti-pattern) |
**Recommendation**: JWT Bearer or Authorization Code + PKCE score highest (90-100/120).
---
## Testing OAuth Flows
### Postman Collection Variables
```json
{
"login_url": "https://login.salesforce.com",
"client_id": "{{CONSUMER_KEY}}",
"client_secret": "{{CONSUMER_SECRET}}",
"redirect_uri": "https://localhost/oauth/callback",
"username": "test@company.com",
"password": "password123"
}
```
### Quick Test: JWT Bearer
```bash
# Generate JWT (requires jq)
JWT=$(python3 -c "
import jwt, time, os
claims = {
'iss': os.getenv('CONSUMER_KEY'),
'sub': 'integration@company.com',
'aud': 'https://login.salesforce.com',
'exp': int(time.time()) + 300
}
with open('server.key') as f:
print(jwt.encode(claims, f.read(), algorithm='RS256'))
")
# Get token
curl -X POST https://login.salesforce.com/services/oauth2/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$JWT" | jq
```
---
## Migration Strategies
### Connected App → External Client App
**Step 1**: Create equivalent ECA
```bash
# Read existing Connected App
Grep: pattern="" path="force-app/main/default/connectedApps/"
# Create new ECA with same scopes
# Use templates: external-client-app.xml, eca-global-oauth.xml
```
**Step 2**: Parallel operation
- Deploy ECA alongside Connected App
- Update one integration at a time
- Monitor both apps
**Step 3**: Cutover
- Update all integrations to use new Consumer Key
- Disable old Connected App
- Archive after 30 days
**Scoring benefit**: ECA typically scores 15-20 points higher due to modern security model.
---
## Quick Reference
### Template Selection by Flow
| Flow | Template File |
|------|---------------|
| Authorization Code (basic) | `connected-app-oauth.xml` |
| JWT Bearer | `connected-app-jwt.xml` |
| Mobile/SPA (PKCE) | `external-client-app.xml` + `eca-global-oauth.xml` |
| Device Authorization | `connected-app-basic.xml` (secret optional) |
| Client Credentials | `eca-oauth-settings.xml` + optional retrieve-first `ecaOauthSecurity` metadata (ECA only) |
### Salesforce OAuth Endpoints
| Environment | Base URL |
|-------------|----------|
| Production | `https://login.salesforce.com` |
| Sandbox | `https://test.salesforce.com` |
| Custom Domain | `https://yourdomain.my.salesforce.com` |
### Key Endpoints
- Authorize: `/services/oauth2/authorize`
- Token: `/services/oauth2/token`
- Revoke: `/services/oauth2/revoke`
- Introspect: `/services/oauth2/introspect`
- Device Code: `/services/oauth2/device/code`
- UserInfo (OpenID): `/services/oauth2/userinfo`
---
## Related Resources
- **Flow Diagrams**: Included above (Authorization Code, JWT Bearer, Device Authorization)
- **Security Checklist**: [references/security-checklist.md](../references/security-checklist.md)
- **Migration Guide**: [references/migration-guide.md](../references/migration-guide.md)
- **Main Skill**: [SKILL.md](../SKILL.md)