Connecting Salesforce to external third-party systems requires secure, standard authorization. The OAuth 2.0 framework is the industry standard for granting applications secure delegated access. When making outbound REST callouts or building custom OAuth token managers in Apex, handling initial token issuance and subsequent refresh cycles properly prevents service downtime and protects sensitive API credentials.
1. Understanding the OAuth 2.0 Token Lifecycle
OAuth 2.0 communication relies on two distinct tokens with complementary security roles:
- Access Token (Short-Lived): Passed in the
Authorization: Bearer <token>header of each HTTP request. It usually expires within minutes to a few hours to limit unauthorized access if intercepted. - Refresh Token (Long-Lived): Securely stored in Salesforce. When an API call returns a
401 Unauthorizedresponse, Apex exchanges the refresh token with the identity provider to obtain a fresh access token without requiring manual user re-login.
- Standard Grants: Authorization Code, Client Credentials, JWT Bearer Token, and Refresh Token grant types.
- Content Type: Token endpoints require
application/x-www-form-urlencodedpayloads. - Modern Best Practice: Use Named Credentials / External Credentials to automate token storage and refreshes with zero custom Apex code whenever possible.
- Security Rule: Never hardcode client secrets or passwords in plain Apex classes; store them in Protected Custom Metadata or Named Credentials.
2. Generating Initial Access Tokens via Apex
When custom authentication management is required, Apex can request tokens by issuing an HTTP POST request to the provider's token endpoint.
Requesting an OAuth access token using client credentials or username-password flows:
public with sharing class OAuthTokenService {
public class TokenResponse {
public String access_token;
public String refresh_token;
public String token_type;
public Integer expires_in;
}
public static TokenResponse requestAccessToken(
String clientId,
String clientSecret,
String username,
String password,
String tokenEndpoint
) {
HttpRequest req = new HttpRequest();
req.setEndpoint(tokenEndpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
// URL-encode all parameter values safely
String body = 'grant_type=password' +
'&client_id=' + EncodingUtil.urlEncode(clientId, 'UTF-8') +
'&client_secret=' + EncodingUtil.urlEncode(clientSecret, 'UTF-8') +
'&username=' + EncodingUtil.urlEncode(username, 'UTF-8') +
'&password=' + EncodingUtil.urlEncode(password, 'UTF-8');
req.setBody(body);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return (TokenResponse) JSON.deserialize(res.getBody(), TokenResponse.class);
} else {
System.debug(LoggingLevel.ERROR, 'Token request failed: ' + res.getStatusCode() + ' - ' + res.getBody());
throw new CalloutException('Failed to retrieve access token: ' + res.getStatus());
}
}
}
3. Implementing the Refresh Token Mechanism
When an existing access token expires, pass the long-lived refresh token back to the identity provider to obtain a fresh access token without sending user credentials again.
public with sharing class OAuthTokenRefresher {
public static String refreshAccessToken(
String clientId,
String clientSecret,
String refreshToken,
String tokenEndpoint
) {
HttpRequest req = new HttpRequest();
req.setEndpoint(tokenEndpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
String body = 'grant_type=refresh_token' +
'&client_id=' + EncodingUtil.urlEncode(clientId, 'UTF-8') +
'&client_secret=' + EncodingUtil.urlEncode(clientSecret, 'UTF-8') +
'&refresh_token=' + EncodingUtil.urlEncode(refreshToken, 'UTF-8');
req.setBody(body);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> payload = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
return (String) payload.get('access_token');
} else {
System.debug(LoggingLevel.ERROR, 'Token refresh failed: ' + res.getBody());
throw new CalloutException('Token refresh rejected: ' + res.getStatus());
}
}
}
4. Declarative Alternative: Named Credentials
While custom Apex token generators work, Salesforce provides native Named Credentials and External Credentials in Setup to handle OAuth flows declaratively.
- Automatic Refresh Handling: Salesforce automatically refreshes expired tokens in the background before callouts are dispatched.
- Encrypted Secret Storage: Credentials and secrets are encrypted at rest and never exposed to Apex code or debug logs.
- Simplified Callout Syntax: Developers call the endpoint directly as
request.setEndpoint('callout:My_External_Service/api/resource')without building manual headers.
5. Common Developer Traps & Security Rules
Hardcoding passwords or client secrets directly into Apex classes exposes sensitive infrastructure credentials in version control and debug logs. Furthermore, the Username-Password flow is being phased out across modern OAuth servers in favor of JWT Bearer and PKCE Authorization flows.
- Always Use EncodingUtil: Wrap every body parameter in
EncodingUtil.urlEncode(param, 'UTF-8')to prevent malformed form-encoded payloads. - Mock Authentication in Unit Tests: Implement
HttpCalloutMockclasses that return mock JSON token payloads to achieve 100% test coverage without invoking live endpoints. - Store Tokens Securely: If caching custom tokens outside Named Credentials, store them in protected custom settings or encrypted cache partitions.
Summary
Handling OAuth 2.0 access and refresh tokens correctly is vital for reliable system integrations in Salesforce. By understanding the token lifecycle, implementing resilient Apex refresh methods, and utilizing Named Credentials for declarative security, developers can build robust, uninterrupted integrations with external enterprise systems.