Skip to main content

OAuth 2.0 Token Generation and Refresh Token Handling in Salesforce Apex

In plain words: An Access Token is like a temporary digital keycard that lets Salesforce call external APIs securely without sending usernames and passwords on every request. Because access tokens expire quickly for safety, a long-lived Refresh Token is used behind the scenes to request a brand-new access token automatically without breaking background integrations.

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 Unauthorized response, Apex exchanges the refresh token with the identity provider to obtain a fresh access token without requiring manual user re-login.
360 OAuth 2.0 Authentication Card:
  • Standard Grants: Authorization Code, Client Credentials, JWT Bearer Token, and Refresh Token grant types.
  • Content Type: Token endpoints require application/x-www-form-urlencoded payloads.
  • 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.

Step 1: Apex Token Generator Service
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.

Step 2: Refreshing Expired Tokens in Apex
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

Developer Trap: Hardcoding User Passwords & Client Secrets
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.
Core Rule: Prefer Salesforce Named Credentials with External Credentials for standard OAuth integrations. When writing custom Apex callouts, always URL-encode parameters and catch 401 Unauthorized responses to trigger token refreshes automatically.
  • 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 HttpCalloutMock classes 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.