It’s an industry best-practice to register credentials for outgoing APIs in External Credentials. This mitigates the risk of password leakage, while significantly reducing the required code. However, not all OAuth Flows are (yet/still) supported by Salesforce. Leaving one to develop code to perform the authentication while fetching the credentials from Custom Metadata or another unencrypted setup location. In this article I’d like to share a setup which allows to leverage External Credentials and a custom Auth. Provider, performing the authentication, while storing the credentials in the most secure way currently possible. Additionally, I’ll share some insights on how to debug your Custom Auth Provider.
(Legacy) OAuth Flows
Open Authentication Flows are secure ways to authenticate, replacing the legacy username-password (basic authentication) and aiming to prevent credentials being hijacked and maliciously used. OAuth defines various ‘flows’/’grant types’ supporting the different needs, like browser flows to authenticate per running user (“login with social”) or server flows to authenticate as a system user for e.g. nightly syncs.
Over time our definition of ‘secure’ evolved, making some grant types (password and implicit flow) undesired to use. They still exist, and can still be applied, though one should be cautious since passwords and access tokens are quite easily accessible.
Because of this, it’s against industry best-practices to continue using this; having larger software companies, like Salesforce, decommissioning their out of the box support for such legacy flows.
Salesforce Authentication
Salesforce, out-of-the-box provides the ability to connect using OAuth via External and Named Credentials, allowing the developer to only need to specify callout:NamedCredentialsName for endpoint. In the background Salesforce will fetch the real endpoint (which can thus differentiate per sandbox), authenticate using the securely stored credentials, and create a centralized management for all your authentications, defined and configured using declarative configurations.
Note: Per Winter ’26 Salesforce added support for the client-credentials flow with the “Configured in an External Auth Identity” option (see Release Notes), making it redundant to setup a Custom Auth Provider.
Currently (Summer ’25) Salesforce External Credentials out-of-the-box support the “JWT Bearer”, “Browser”, “Client credentials with Client Secret” and “Client Credentials with JWT Assertion” Flows. Implying that if you’d need to connect using Password- (legacy) or Client-Credentials Flow, there isn’t a declarative way to set this up.
The following design describes a setup which allows to still store the credentials in a safe way and leverage out-of-the box authentication functionalities, avoiding otiose code, and having ‘hardcoded’ passwords/ secrets somewhere readable in setup or even in your code-repository (like a Custom Metadata record).
The design
Note: While below design provides a secure way to implement legacy OAuth Flow, it is always best to first try and challenge the external identity provider to a more secure way of authentication. If that isn’t possible, while the API connection is required, one can follow below design.
This solution design consists of two External/Named Credentials and one Custom Auth Provider with an Apex class and Custom Metadata object. Together, it authenticates, while reducing the full authentication complexity for the developer, as one only requires to specify the named credential on the endpoint (callout:).
- Named Credential 1 – specifies the API endpoint we’d like to call and how we authenticate (the External Credential);
- External Credential – references the custom Auth Provider and the user-permission Principle;
- Auth Provider – specifies the Apex class to perform the effective authentication and the Request Body which should be populated based on credentials from the second (legacy) Named Credentials;
- Named Credential 2 – specifies the authentication URL, and the (securely saved) credentials.
Logic: Upon HTTPRequest execution, Salesforce will fetch the credentials (4), parse this using the merge fields into the Request Body (3), let the Custom Auth Provider code perform it’s magic (3), fetch the access token and add this to the Authorized Header (2) for the API callout (1).
Confused? No need! Once you’ve seen it, it will all make sense.
1+2. Named and External Credential
It all starts with a Named Credential which can be called from Apex, detailing the API endpoint and referencing the External Credential for authentication (callout:ABC_Product).

The External Credential sets up a Browser OAuth Flow, but defines a custom Auth Provider. Letting Salesforce authenticate with its own OAuth flow and then perform the authentication-callout.
Additionally, the External Credential defines a Principle, which allows to grant explicit access to a user (group) by assigning a Permissionset with the External Credential Principle, matching this.

3. (Custom) Auth Provider
A Custom Auth Provider consists of three components:
- Custom Metadata Type – allowing the storage of ‘auth provider tailored fields’;
- Apex Plugin Class – defining the logic of authentication and OAuth flow handling;
- Auth Provider – connecting the metadata record values with the apex class; and specifying as per which user the logic should be ran by.
3.1. Custom Metadata Type
The metadata type is always required by Salesforce, but it is up to the developer to decide what fields to create and leverage in the Apex Plugin Class. For this implementation I’ve set up three fields:
- Callback URL – the Auth Providers callback-url, redirecting from
initiate()tohandleCallback()
One can simply copy the relative part of the “Callback URL” in Salesforce Configuration (see screenshot in 3.3), and append the state parameter (as explained in 3.2); - Named Credential – the Developername of the Named Credentials (#4) where the credentials are securely stored;
- Request Body – describing the body, including the merge fields to ‘inject’ the credentials from the Named Credential (#4). This can be a JSON object for client-credentials, or a URL construct for password-grant.
3.2. Apex Plugin Class
The class extends Auth.AuthProviderPluginClass and should implement a couple methods which support the different phases in OAuth. For this implementation we’ve set up:
getCustomMetadataType()– defines the metadata type for this AuthProvider.
The values of the metadata record matching the Auth Provider name (default created) are made available to each method via the input parameterMap<String, String> authProviderConfig;initiate()– the idea is to ‘skip’ this phase by redirecting to the callback url of the Auth Provider, which then triggers the next method. Note, Salesforce only processes callback-requests which reference a state which was previously generated by Salesforce during the initiate-method and is thus required to make it work;handleCallback()– performs the authentication of choice, returning the access, refresh token and state;getUserInfo()– method to optionally fetch more user-information from the external system, allowing to update data in Salesforce, e.g. when users are mastered in another system.
For sake of simplicity, below snipped is trimmed of detailed/method comments and Logging.
global with sharing class SRV_AuthProvider_ABC extends Auth.AuthProviderPluginClass{
private static final String CLASS_NAME = 'SRV_AuthProvider_ABC';
global String getCustomMetadataType(){
return 'Integration_Setting__mdt';
}
global PageReference initiate( Map<String, String> authProviderConfig, String stateToPropagate ){
return new PageReference( authProviderConfig.get( 'Callback_URL__c' ) + stateToPropagate );
}
global Auth.AuthProviderTokenResponse handleCallback( Map<String, String> authProviderConfig, Auth.AuthProviderCallbackState state ){
// Prepare the callout with endpoint, method and body
SRV_Request.HTTP_SERVICE requestService = ( SRV_Request.HTTP_SERVICE ) SRV_Request.newInstance(
SRV_Request.REQUEST_TYPES.HTTP,
'callout:' + authProviderConfig.get( 'Access_Token_NamedCredentials__c' ),
'SRV_AuthProvider_ABC'
);
HttpRequest request = requestService.getRequest();
request.setMethod( App_HTTP.METHOD_POST );
request.setHeader( App_HTTP.HEADER_CONTENTTYPE, App_HTTP.HEADER_CONTENTTYPE_FORM );
request.setBody( authProviderConfig.get( 'Request_Body__c' ) );
// Perform the callout, and set response or log failures/exception
AuthResponseWrapper responseWrapper;
try{
HttpResponse res = ( HttpResponse ) requestService.send();
if( res.getStatusCode() == App_HTTP.getStatusCode( App_HTTP.STATUS.OK ) ){
responseWrapper = ( AuthResponseWrapper ) JSON.deserialize( res.getBody(), AuthResponseWrapper.class );
} else{
throw new fflib.InvalidInputValueException( ( ( FailedResponseWrapper ) JSON.deserialize( res.getBody(), FailedResponseWrapper.class ) ).getErrorString() );
}
} catch( Exception ex ){
// Log captured Exception
return null;
}
return new Auth.AuthProviderTokenResponse( CLASS_NAME, responseWrapper.access_token, responseWrapper.refresh_token, state.queryParameters.get( 'state' ) );
}
global Auth.UserData getUserInfo( Map<String, String> authProviderConfig, Auth.AuthProviderTokenResponse response ){
return new Auth.UserData(
null, // External Identity Provider User ID
null, // FirstName
null, // LastName
null, // FullName
null, // Email
null, // Link
null, // Username
null, // Locale
response.provider, // Provider
null, // SiteLoginURL
new Map<String, String>() // AttributeMap
);
}
private class AuthResponseWrapper{
private String access_token;
private Integer expires_in;
private Integer refresh_expires_in;
private String refresh_token;
private String token_type;
private String scope;
}
private class FailedResponseWrapper{
private String error;
private String error_description;
private String getErrorString(){
return this.error + ': ' + this.error_description;
}
}
}Note, in handleCallback() the endpoint is set to callout:[named-credential] which is fetched from the custom metadata record of the auth provider. The body is set with the Request_Body__c, which contains the Merge Fields. Upon execution Salesforce will substitute the merge fields, with the values from the defined Named Credential and perform the callout. Without the password/secret being available to logs, users, etc.
3.3. Auth Provider
The declarative setup to connect the Apex class, the custom metadata fields and as per which user the logic should be executed by.
Below screenshot shows the setup for a password-grant OAuth flow, though one could of course also specify a JSON-object in case this is required by the external identity provider. Most important is that the Request Body in metadata should match with the ContentType which is set to the Request Header in code.

4. Legacy Named Credential for Credential storage
When entering Named Credentials in Salesforce Classic it’s still possible to define a “Password Authentication” which will show the input fields for username and password. After saving it, the password is stored, but never readable for any system admin.
Three things are important to be aware of for this Named Credential:
- Depending on the need one can store username-password, but of course also client id-secret (the merge-field-names will remain identical);
- Since you can’t
URLEncodethe credentials in Apex, it is crucial to store the username and password alreadyURLEncoded. Reason is that Salesforce protects the merge fields, and it’s thus impossible to wrap anything around the merge fields, withoutURLEncodingthe full endpoint; - It’s important to check the “Allow Merge fields in HTTP Body” to allow substitution in the body.

5. Enablement and debugging
Once everything is setup, it is important to:
- Ensure the Principle is Authenticated/Configured:
- Go to the External Credential and click the Actions dropdown at the Principle, there you can select “Authenticate”;
- This will trigger the authentication flow, redirecting to Salesforce (callback URL), then performing the callout, and if an access token is successfully fetched, the principle will be updated to “Configured”.
- Ensure the desired users are granted permission to the External Credential Principle.
Debugging your Custom Auth Provider
Once you’ve specified the Named Credential (#1) in your code (callout: endpoint), Salesforce will take full ownership and control of the authentication. Implying ‘swallowing’ all Logs regarding this. Even when putting stack traces on your Apex Class (#3.2) or the running user of the Auth Provider (#3.3), you won’t find a debug log.
The only way to properly debug is to:
- Set a stack trace on the Auth Provider “Execute As” user;
- Trigger the Authentication using the “Authenticate” action on the External Credential Principle.
Then you’ll be able to see debug logs for that particular user, detailing potential issues/exceptions captured.
Conclusion
While the solution might feel complex, it enables you to use a legacy or not-yet supported OAuth Grant Type in Salesforce, while:
- storing your secrets/passwords in the most secure way; and avoid unencrypted/unsecure raw storage anywhere in your setup or even repository;
- using named credentials to offload all authentication and reduce repetitive code (and keeping the codebase clean and clear);
- centralizing all your authentications in a declarative way in External Credentials.
With this article I’d like to invite you to:
- Always first challenge the external identity provider whether they can’t migrate to a more secure/different OAuth Flow;
- Always challenge solutions where credentials/secrets are stored in custom metadata;
- or where custom authentication logic is put in code, making it harder to maintain or scale.
Hope this allows you to satisfy the business demand, while adhering to best practices.
Stay secure and happy coding!