Developers / Platform

How to Set Up Persisting OAuth Tokens in Salesforce

By Alec Dorner

Most developers who have worked on integrations are familiar with the OAuth 2.0 web server authentication flow for authenticating with external APIs. At a high level, the Salesforce application makes a callout to the external API providing credentials to request an access token. Those credentials are verified, and a token is issued by the external API. 

Unfortunately, on the Salesforce platform, there wasn’t a great way to persist an OAuth token across transactions for its lifetime. More often than not, this led to applications set up to request that token each transaction, which uses unnecessary resources and makes the application less efficient. However, with the Winter ‘23 release, Salesforce introduced external credentials. Along with external credentials, came custom headers, permission set mapping, and authorization parameters within permission set mappings. This combination now provides a secure, native, and easy-to-use mechanism to store an authentication token and its valid-through date/time.

Getting Set Up

The first step in this process is to create an external credential. Go to Setup and enter ‘Named Credentials’ in the Quick Find box. Click on Named Credentials, and you should see two tabs, Named Credentials and External Credentials. Go to the External Credentials tab and click New.

Add your label and your name and select Custom for your Authentication Protocol.

The next step is to create a permission set mapping. You can map to an existing permission set or create a new one, either should work for the purposes of this walkthrough. 

Under Permission Set Mappings, hit New. You don’t need to worry about adding authentication parameters as the code will handle the generation of those parameters.

READ MORE: Learn Salesforce Roles and Profiles in 5 Minutes (Ft. Permission Sets)

Now, the nice piece about this methodology is that you can create custom headers using a formula. Let’s imagine that this API just had you pass the token as the authorization header. Instead of having to specify this in the code making the callout, we can actually use a custom header formula here as displayed below. Under the Custom Headers section, click New and enter the relevant authorization header.

That’s it for the external credential! We’re all set. Now, let’s create a named credential. 

Go back to Named Credentials and go to the Named Credentials tab now. Click New and configure your named credential. Since we are generating an authorization header using a formula in a custom header, we need to make sure to turn off ‘Generate Authorization Header’ and to turn on ‘Allow Formulas in HTTP Header’ like shown below.

As far as configuration is concerned, we’re now all set! 

Grabbing the Code

Now, we need to walk through the code a bit. However you create apex classes, create a new class called ConnectAPI and drop the below code into it.

code

public with sharing class ConnectAPI {
    
    private final String NAMED_CRED_RELATIVE_URI = '/services/data/v56.0/named-credentials/credential';

    public HttpResponse updateNamedCredential(Map<String, Object> input, Boolean mappingExistsAlready){

        HttpRequest req = new HttpRequest();

        req.setEndpoint(Url.getOrgDomainUrl().toExternalForm() + NAMED_CRED_RELATIVE_URI);
        req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
        req.setHeader('Content-Type', 'application/json');
        
        if(mappingExistsAlready){
            req.setMethod('PUT');
        }
        else{
            req.setMethod('POST');
        }
        
        req.setTimeout(120000);
        req.setBody(JSON.serialize(input));

        return new Http().send(req);
    }
}

code

READ MORE: 5 Mistakes to Avoid When Learning Apex Code

One thing to note here is that you MUST use v56.0 or later for the API version for the callout endpoint. The reason for this is that this whole thing was released with that API version in Winter ’23, so it is not available earlier than that. Now that we have that class set up, create another class called TokenHelper and drop the below code in there.

code

public with sharing class TokenHelper {

    private static final String CREDENTIALNAME_TOKEN = 'token';
    private static final String CREDENTIALNAME_EXPIRES = 'expires';

    public static Boolean checkTokenValidity(String externalCredName){

        ConnectApi.ExternalCredential externalCred = ConnectApi.NamedCredentials.getExternalCredential(externalCredName);

        if(externalCred != null && !externalCred.principals.isEmpty()){

            ConnectApi.Credential cred = ConnectApi.NamedCredentials.getCredential(externalCredName, externalCred.principals[0].principalName, ConnectApi.CredentialPrincipalType.NamedPrincipal);

            if(cred != null && cred.credentials.containsKey(CREDENTIALNAME_TOKEN) && cred.credentials.containsKey(CREDENTIALNAME_EXPIRES)){

                ConnectApi.CredentialValue expires = cred.credentials.get(CREDENTIALNAME_EXPIRES);

                return (expires.value != null && Datetime.valueOf(expires.value) > Datetime.now());
            }
        }

        return false;
    }
    

    public static void updateToken(String externalCredName, String newToken, Datetime expires){

        ConnectApi.ExternalCredential externalCred = ConnectApi.NamedCredentials.getExternalCredential(externalCredName);

        if(externalCred != null && !externalCred.principals.isEmpty()){

            ConnectApi.Credential cred = ConnectApi.NamedCredentials.getCredential(externalCredName, externalCred.principals[0].principalName, ConnectApi.CredentialPrincipalType.NamedPrincipal);

            if(cred != null){

                Boolean mappingExistsAlready = true;

                Map<String, Object> input = convertCredToInput(cred);

                if(((Map<String, Map<String, Object>>)input.get('credentials')).keySet().isEmpty()){
                    mappingExistsAlready = false;
                }

                ((Map<String, Map<String, Object>>)input.get('credentials')).put(CREDENTIALNAME_TOKEN, new Map<String, Object>{
                        'encrypted' => true,
                        'value' => newToken
                });

                ((Map<String, Map<String, Object>>)input.get('credentials')).put(CREDENTIALNAME_EXPIRES, new Map<String, Object>{
                        'encrypted' => false,
                        'value' => String.valueOf(expires)
                });

                ConnectAPI conAPI = new ConnectAPI();

                HttpResponse resp = conAPI.updateNamedCredential(input, mappingExistsAlready);           
            }   
        }
    }



    public static Map<String, Object> convertCredToInput(ConnectApi.Credential cred){

        Map<String, Object> input = new Map<String, Object>();
        input.put('authenticationProtocol', String.valueOf(cred.authenticationProtocol));
        input.put('externalCredential', String.valueOf(cred.externalCredential));
        input.put('principalName', String.valueOf(cred.principalName));
        input.put('principalType', String.valueOf(cred.principalType));

        Map<String, Map<String, Object>> credentials = new Map<String, Map<String, Object>>();

        for(String credKey : cred.credentials.keySet()){
            
            ConnectApi.CredentialValue credValue = cred.credentials.get(credKey);

            credentials.put(credKey, new Map<String, Object>{
                'value' => credValue.value,
                'encrypted' => credValue.encrypted
            });

        }

        input.put('credentials', credentials);

        return input;
    }
}

code

Walking Through the Code

There are a couple methods in the TokenHelper class that we will walk through because they are the heart of this implementation. The first method called checkTokenValidity does exactly that. It is going to take the name of an external credential and check whether the token is valid. The way this method will define a token as invalid is any one of the below: 

  1. No external credential exists with the specified name.
  2. The external credential exists but is not mapped to any permission sets.
  3. The external credential exists and is mapped to a permission set, but the permission set mapping does not contain expiration datetime or token authorization parameters.
  4. The external credential exists, it is mapped to a permission set, and the permission set mapping contains expiration datetime and token authorization parameters, but the expiration datetime is earlier than the current datetime. 

My suggestion is that you actually modify this code to throw an error if one of the first two conditions is true to ensure your code is not assuming you’re falling into either of the last two, which are the only situations in which you should attempt to do anything related to getting a new token. However, we will follow the happy path. 

If the token parameter has not been set yet or the token expiration datetime has passed, this method will let you know.

The second method is called updateToken, which, again, is doing exactly what it sounds like it’s doing. This method is called when you’ve made your token callout, gotten your new token and expiration datetime, and you want to set those values on the external credential mapping before you make your callout. This circles back to the ConnectAPI class that we created earlier. 

The reason why we’re updating the named credential via the Connect REST API instead of just using the ConnectApi.NamedCredentials class is because using that class to update it will cause us to bump into the “You have uncommitted work pending” callout exception when we make the actual callout after updating the token. I know this because I bumped into it when I first implemented it that way. I was testing things out piecewise and everything worked great. Then, when I went to put it all together, I got hit. However, if you don’t need to use the token in the same transaction, I would suggest you use the ConnectApi.NamedCredentials class, which is a bit more straightforward than the hoops we are jumping through here.

In this method, we will also make use of the convertCredToInput helper method that handles converting an existing named credential entry into a map. The reason we are creating a map and are not just creating an instance of ConnectApi.CredentialInput is because we cannot serialize that object using the JSON.serialize method, so I figured building a map was a bit less involved than using the JSON Generator class, but you’re more than welcome to modify as desired. 

Another thing I want to call out is that for the “expires” authorization parameter, we are setting encrypted to false. You actually can’t do this in the user interface to my knowledge, and you need that value not to be encrypted or else your code can’t access the value (it will just come across as null). That being said, if you use this methodology to persist OAuth tokens, don’t bother creating those authorization parameters, and just let the code set it up for you to make sure everything lines up.

Summary

That’s it! By having this framework in place, you can inject it into your regular callout code, making use of named credentials and storing authentication details in a more secure and appropriate manner. In your code, just use the TokenHelper.checkTokenValidity method to see if you need a new token, and if so, make your token callout and store the token and new expiration date using the TokenHelper.updateToken method.

The Author

Alec Dorner

Alec, a Salesforce Architect working at Cenegenics, is the creator of Flow Analyzer and the founder of Force for Change.

Leave a Reply to Yusuf Kaydawala Cancel reply

Comments:

    Yusuf Kaydawala
    June 02, 2023 8:28 pm
    This is a great overview of External Credentials. Thanks
    Richard Wintle
    March 08, 2024 2:23 pm
    This is an excellent post. However, I am struggling with test class code. There appears to be limited information on ConnectApi "setTest" usage. Any instruction on that would be helpful.