Secure, auditable, and maintainable authentication is the foundation of every reliable Salesforce integration. For data migration, where millions of records may transit through a single integration user credential over hours or days, the authentication architecture is not an afterthought; it is a first-class engineering concern.
In Part 1 of this series, we covered org readiness API limits, metadata inventory, and storage projections. This article covers the three authentication patterns that matter most for Salesforce data migration: OAuth 2.0 Client Credentials Flow, JWT Bearer Token Flow, and Named Credentials, and how to set up your Connected App correctly before any of them work the way you need.
Table of Contents
Connected App Configuration
A Salesforce Connected App is the OAuth 2.0 client registration that authorises an external application to interact with your org. For a data migration integration, the Connected App should be purpose-built for the migration workload and retired or deactivated after completion.
| Connected App Setting | Recommended Value | Rationale |
|---|---|---|
| OAuth Scopes | api, bulk_api, refresh_token | Minimum scopes for Bulk API 2.0 and REST access |
| IP Relaxation | Relax IP restrictions (migration servers) | Allows migration server IPs without whitelist churn |
| Refresh Token Policy | Expire refresh token after 90 days | Limits exposure without forcing re-auth during migration |
| Require Secret for Web Server | Enabled | Prevents unauthorised client_credentials flows |
| Enable Client Credentials Flow | Enabled (for server-to-server) | Removes user interaction from automated migration jobs |
| Permitted Users | Admin approved users are pre-authorised | Limits OAuth to the migration integration user only |
OAuth 2.0 Flows for Server-to-Server Migration
Client Credentials Flow (Recommended for Automation)
The OAuth 2.0 Client Credentials Flow is the recommended authentication pattern for fully automated, server-to-server migration jobs. Unlike the Web Server Flow (which requires a browser redirect and user interaction) or the JWT Bearer Token Flow (which requires certificate management), the Client Credentials Flow is operationally simpler: the external application exchanges its client_id and client_secret for an access token directly, with no user session involved.
OAuth 2.0 Client Credentials Flow: Salesforce Token Exchange
+--------------------------+ +----------------------------------+
| External Migration App | | Salesforce Platform |
+-----------+--------------+ +-------------+--------------------+
| |
| POST /services/oauth2/token |
| grant_type=client_credentials |
| client_id= |
| client_secret= |
|-------------------------------------->|
| |
| 200 OK |
| { |
| access_token: 'XXXXXX', |
| instance_url: 'https://org.sf.com',|
| token_type: 'Bearer', |
| scope: 'api bulk_api' |
| } |
|<--------------------------------------|
| |
| Authorization: Bearer |
| (All subsequent API calls) |
|-------------------------------------->|
// Node.js: OAuth 2.0 Client Credentials Token Acquisition
const axios = require('axios');
const qs = require('querystring');
async function getAccessToken(config) {
const response = await axios.post(
`${config.sfLoginUrl}/services/oauth2/token`,
qs.stringify({
grant_type : 'client_credentials',
client_id : config.clientId,
client_secret: config.clientSecret,
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return {
accessToken : response.data.access_token,
instanceUrl : response.data.instance_url,
expiresAt : Date.now() + 3600 * 1000, // 1-hour TTL
};
}
// Singleton token cache: avoids re-authentication on every API call
let tokenCache = null;
async function getValidToken(config) {
if (!tokenCache || Date.now() >= tokenCache.expiresAt - 60000) {
tokenCache = await getAccessToken(config);
console.log(`[Auth] Token refreshed at ${new Date().toISOString()}`);
}
return tokenCache;
}
JWT Bearer Token Flow (Certificate-Based)
For organisations with strict secret management policies that prohibit long-lived client secrets, the JWT Bearer Token Flow provides a certificate-based alternative. The external application signs a JWT assertion using an RSA private key, and Salesforce validates the assertion against the public certificate registered in the Connected App. This approach integrates naturally with enterprise secret management solutions such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault.
# Python: JWT Bearer Token Flow
import jwt, time, requests
from cryptography.hazmat.primitives import serialization
def get_jwt_access_token(config: dict) -> dict:
with open(config['private_key_path'], 'rb') as f:
private_key = serialization.load_pem_private_key(f.read(), password=None)
now = int(time.time())
payload = {
'iss': config['client_id'], # Connected App consumer key
'sub': config['integration_user'], # Integration user's username
'aud': 'https://login.salesforce.com',
'exp': now + 300, # Token valid for 5 minutes only
}
assertion = jwt.encode(payload, private_key, algorithm='RS256')
resp = requests.post(
f"{config['sf_login_url']}/services/oauth2/token",
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' : assertion,
},
headers={'Content-Type': 'application/x-www-form-urlencoded'},
)
resp.raise_for_status()
return resp.json() # { access_token, instance_url, token_type }
Named Credentials: Decoupling Authentication from Code
Salesforce Named Credentials provide a platform-native mechanism for storing endpoint URLs and authentication credentials in a secure, version-controlled configuration object rather than hardcoding them in Apex code or embedding them in environment variables. For migrations that include Salesforce-to-Salesforce callouts (for example, a post-migration validation Apex class invoking an internal REST service), Named Credentials are the recommended pattern.
Named Credentials eliminate the need to handle OAuth token management in code because the Salesforce platform automatically handles token acquisition, caching, and renewal transparently. An Apex callout that references a Named Credential will always use a valid, non-expired token without any developer-managed refresh logic.
Named Credential Architecture: Apex Callout Flow
+-------------------------------------------------------------------+
| Salesforce Org |
| |
| +------------------+ callout:MigrationAPI/endpoint |
| | Apex Class |-----------------------------------------> |
| | (Validator.cls) | ^ |
| +------------------+ Named Credential | |
| +-------------------+ | |
| | Label: MigrationAPI| | |
| | URL: https://api +--+ |
| | Auth: OAuth 2.0 | (token managed |
| | (Client Creds) | by platform) |
| +-------------------+ |
+-------------------------------------------------------------------+
|
v
+------------------------+
| External Migration API |
| (REST: HTTPS / TLS 1.3) |
+------------------------+
// Apex: Callout using Named Credential
public class MigrationValidatorCallout {
private static final String NAMED_CRED = 'callout:Migration_Orchestration_API';
public static Map validateBatch(String batchId) {
HttpRequest req = new HttpRequest();
req.setEndpoint(NAMED_CRED + '/v1/batches/' + batchId + '/validate');
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(30000); // 30-second timeout
Http http = new Http();
HttpResponse res = http.send(req);
// Platform automatically injects: Authorization: Bearer
if (res.getStatusCode() != 200) {
throw new CalloutException('Validation API returned: ' + res.getStatusCode());
}
return (Map) JSON.deserializeUntyped(res.getBody());
}
}
Which Flow Fits Which Scenario
| Scenario | Recommended Pattern |
|---|---|
| Automated bulk load from external app | Client Credentials Flow |
| Regulated industry, no stored secrets allowed | JWT Bearer Token Flow |
| Post-migration Apex validation callouts | Named Credentials |
| Admin-triggered or interactive migration tool | Web Server Flow (user context required) |
Security Checklist Before You Run Anything
A few things to confirm before the first migration job starts:
- Create a dedicated integration user with only the permissions the migration needs — not a full System Administrator profile
- Store client_secret and private keys in a secret manager, never in .env files or source code repositories
- Enable Salesforce Event Monitoring before migration starts, so you capture API-level audit logs from day one
- Confirm the Connected App’s Permitted Users setting is locked to the migration user
- Document the Connected App’s expiry settings and add a post-cutover task to deactivate it
- Run a test token acquisition and a single test API call against a sandbox before going anywhere near production
That last point sounds obvious, but it’s easy to skip when the project timeline is tight. An authentication failure at T-zero of your cutover window is not where you want to diagnose a scope misconfiguration.
What’s Next in This Series
Getting your OAuth setup right is table stakes. The next article is where things get interesting.
Part 3 will cover the Bulk API 2.0 job lifecycle, how to create jobs, stream CSV data at scale, poll for completion without hammering the API, and handle partial failures without losing records or corrupting your load state. If you’ve ever watched a migration job finish and had no idea whether it actually succeeded, that article is for you.
Frequently Asked Questions (FAQ)
Client Credentials Flow authenticates using a client ID and client secret, a shared secret model. JWT Bearer Token Flow uses a signed JWT assertion backed by an RSA key pair, so no shared secret is involved. JWT is the better choice when your security policy prohibits stored secrets or requires certificate-based trust. For most automation scenarios, Client Credentials Flow is simpler to operate.
No. Named Credentials are Apex-native and can only be referenced from Apex code running inside the Salesforce platform. External applications need to handle their own OAuth token management using Client Credentials or JWT Bearer Token flows.
For a Bulk API 2.0 migration, api, bulk_api, and refresh_token are sufficient. Avoid broader scopes like full unless you have a specific reason for them.
If your token expires mid-job, the API calls return a 401 Unauthorized response. The solution is proactive token caching: check the token’s expiry timestamp before each API call and refresh it about 60 seconds before expiry. Bulk API jobs themselves are not interrupted by token expiry; the job runs server-side, but your polling and result-retrieval calls will fail without a valid token.
Deactivate it. Deletion removes the audit record along with the Connected App itself. Deactivation blocks any further token issuance while preserving the configuration and history for compliance purposes.

Kiran Sreeram Prathi
I’m Kiran Sreeram Prathi, a Salesforce Developer dedicated to building scalable, intelligent, and user-focused CRM solutions. Over the past five years, I’ve delivered Salesforce implementations across healthcare, finance, and service industries—focusing on both technical precision and user experience. My expertise spans Lightning Web Components (LWC), Apex, OmniStudio, and Experience Cloud, along with CI/CD automation using GitHub Actions and integrations with platforms such as DocuSign, Conga, and Zpaper. I take pride in transforming complex workflows into seamless digital journeys and implementing clean DevOps strategies that reduce downtime and accelerate delivery. Recognized by organizations like Novartis, WILCO, and Deloitte, I enjoy solving problems that make Salesforce work smarter and scale better. I’m always open to connecting with professionals who are passionate about process transformation, architecture design, and continuous innovation in the Salesforce ecosystem.
- This author does not have any more posts.







