People working on laptops

Named Credentials OAuth Password Flow

by Nikita Verkhoshintcev

I recently had to work on the named credentials to integrate the third-party tool into Salesforce.

The named credentials are the recommended and secure way to store credentials and make it easy to make callouts.

Unfortunately, the integration I've been working on supported only the password grant type OAuth flow.

I searched the Internet for a solution, and it seemed that many people were also looking for the same thing.

I decided to share my experience and how I eventually solved it.

Kudos to Alec for this article on SalesforceBen. It helped me a lot by guiding me to a possible solution. I recommend checking it out.

Let's get started!

Technical Design

First, as mentioned before, the password flow is unavailable out of the box. So, we need to set up a custom token refresh functionality to have a persistent connection.

The main idea is to set up a token refresh named credential that obtains the access token and stores it, along with the expiration timestamp, in the external credential variables.

Before each callout, we will verify if the token is still valid. If not, we will refresh the token and update the external credential variables.

The other named credentials could use that token in their authorization headers to make the callouts.

Salesforce Configuration

First, we need to create the named credential for the token endpoint. We will use it to obtain the access token.

Create a named credential with the external credential for the token endpoint and choose the custom authentication protocol.

Create a principal and set the authentication parameters. With the password grant type, you need the following:

  • grant_type
  • client_id
  • client_secret
  • username
  • password
  • scope

Then, let's create another named credential for the callouts.

Create a principal, but do not set any authentication parameters.

In the SF UI, you can define only encrypted values, but we would have to put the non-encrypted value for the token timestamp to access it in the Apex.

Note: You cannot access credentials in Apex directly as long as they are encrypted.

Set the authorization custom header to Bearer {!$Credential.Callout.access_token}.

Apex Implementation

Next, let's create the utility class to handle the token refresh.

As mentioned above, we must validate and update the token before each callout.

Remember, you cannot commit to the database before the callout.

In the SalesforceBen article, they used Rest API to update the authentication parameters for the external credentials.

Unfortunately, so far as I was testing that, it didn't work in my case because it was still complaining about the commit before the callout error.

I solved it by verifying the token and doing the next callout in two separate transactions.

In my case, I just validated the token and made a callout in two separate LWC functions. But, the alternative could be, for example, a queueable.

Let's assume we have this sample class with the method to validate the token.

public with sharing class TokenService {
  // Sample Named Credential to obtain the token
  private static final String TOKEN_NC = 'Token';
  // Sample Named Credential to make the callout in your implementation
  private static final String CALLOUT_NC = 'Callout';

  public static void validateAccessToken() {
    if (!NamedCredentialUtils.checkTokenValidity(TOKEN_NC)) {
      // Retrieve the access token
      OAuthTokenResponse tokenResponse = getAccessToken();
      // Get the expiration datetime
      Datetime expires = Datetime.now().addSeconds(Integer.valueOf(tokenResponse.expires_in));
      // Update the credential
      NamedCredentialUtils.updateToken(CALLOUT_NC, tokenResponse.access_token, expires);
    }
  }

  public static OAuthTokenResponse getAccessToken() {
    HttpRequest request = new HttpRequest();
    HttpResponse response = new HttpResponse();
    Http http = new Http();

    if (Test.isRunningTest()) {
      request.setEndpoint('https://callout:' + TOKEN_NC + '.com');
    } else {
      request.setEndpoint('callout:' + TOKEN_NC);
    }

    request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
    request.setMethod('POST');
    request.setBody(
      String.join(
        new List<String>{
          'grant_type=password',
          'client_id={!$Credential.Token.client_id}',
          'client_secret={!$Credential.Token.client_secret}',
          'username={!$Credential.Token.username}',
          'password={!$Credential.Token.password}',
          'scope={!$Credential.Token.scope}'
        },
        '&'
      )
    );
    request.setTimeout(60000);
    response = http.send(request);

    return (OAuthTokenResponse) JSON.deserialize(response.getBody(), OAuthTokenResponse.class);
  }

  public class OAuthTokenResponse {
		public String access_token { get; set; }
		public String token_type { get; set; }
		public String expires_in { get; set; }
	}
}

Let's review it piece by piece.

The token is valid if the credential is present, has the variables, and the expiration date and time are in the future.

public static Boolean checkTokenValidity(String externalCredName) {
  ConnectApi.Credential cred = getCredential(externalCredName);

  if (
    cred != null &&
    cred.credentials.containsKey(CREDENTIAL_ACCESS_TOKEN) &&
    cred.credentials.containsKey(CREDENTIAL_EXPIRES)
  ) {
    ConnectApi.CredentialValue expires = cred.credentials.get(CREDENTIAL_EXPIRES);

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

  return false;
}

We can use ConnectApi to retrieve the external credential.

public static ConnectApi.Credential getCredential(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
    );
    return cred;
  }

  return null;
}

We need to obtain a new one if the token is invalid via TokenService.getAccessToken method.

Then, we can update the authentication parameters via the ConnectApi.

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 (externalCred != null && !externalCred.principals.isEmpty()) {
      if (cred == null) {
        return;
      }
      ConnectApi.CredentialInput input = new ConnectApi.CredentialInput();
      input.authenticationProtocol = cred.authenticationProtocol;
      input.authenticationProtocolVariant = cred.authenticationProtocolVariant;
      input.externalCredential = cred.externalCredential;
      input.principalName = cred.principalName;
      input.principalType = cred.principalType;
      input.credentials = new Map<String, ConnectApi.CredentialValueInput>();

      ConnectApi.CredentialValueInput accessTokenInput = new ConnectApi.CredentialValueInput();
      accessTokenInput.encrypted = true;
      accessTokenInput.value = newToken;

      input.credentials.put(CREDENTIAL_ACCESS_TOKEN, accessTokenInput);

      ConnectApi.CredentialValueInput expiresInput = new ConnectApi.CredentialValueInput();
      expiresInput.encrypted = false;
      expiresInput.value = String.valueOf(expires);

      input.credentials.put(CREDENTIAL_EXPIRES, expiresInput);

      if (cred.credentials.isEmpty()) {
        ConnectApi.NamedCredentials.createCredential(input);
      } else {
        ConnectApi.NamedCredentials.patchCredential(input);
      }
    }
  }
}

Here is what the complete named credential utility class looks like.

/**
 * Utility class to handle the access tokens for Named Credentials
 */
public with sharing class NamedCredentialUtils {
	private static final String CREDENTIAL_ACCESS_TOKEN = 'access_token';
	private static final String CREDENTIAL_EXPIRES = 'expires';
	/**
	 * Get External Credential instance by the name
	 */
	public static ConnectApi.Credential getCredential(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
			);
			return cred;
		}

		return null;
	}
	/**
	 * Check whenever the access token in the given External Credential is valid.
	 */
	public static Boolean checkTokenValidity(String externalCredName) {
		ConnectApi.Credential cred = getCredential(externalCredName);

		if (
			cred != null &&
			cred.credentials.containsKey(CREDENTIAL_ACCESS_TOKEN) &&
			cred.credentials.containsKey(CREDENTIAL_EXPIRES)
		) {
			ConnectApi.CredentialValue expires = cred.credentials.get(CREDENTIAL_EXPIRES);
      // Note: we can access expires value because it is public.
			return (expires.value != null && Datetime.valueOf(expires.value) > Datetime.now());
		}

		return false;
	}
	/**
	 * Update the access token in the given External Credential and store the expiration datetime.
	 */
	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 (externalCred != null && !externalCred.principals.isEmpty()) {
				if (cred == null) {
					return;
				}
				ConnectApi.CredentialInput input = new ConnectApi.CredentialInput();
				input.authenticationProtocol = cred.authenticationProtocol;
				input.authenticationProtocolVariant = cred.authenticationProtocolVariant;
				input.externalCredential = cred.externalCredential;
				input.principalName = cred.principalName;
				input.principalType = cred.principalType;
				input.credentials = new Map<String, ConnectApi.CredentialValueInput>();

				ConnectApi.CredentialValueInput accessTokenInput = new ConnectApi.CredentialValueInput();
				accessTokenInput.encrypted = true;
				accessTokenInput.value = newToken;

				input.credentials.put(CREDENTIAL_ACCESS_TOKEN, accessTokenInput);

				ConnectApi.CredentialValueInput expiresInput = new ConnectApi.CredentialValueInput();
				expiresInput.encrypted = false; // we set it as public so that we can access it via Apex
				expiresInput.value = String.valueOf(expires);

				input.credentials.put(CREDENTIAL_EXPIRES, expiresInput);

				if (cred.credentials.isEmpty()) {
					ConnectApi.NamedCredentials.createCredential(input);
				} else {
					ConnectApi.NamedCredentials.patchCredential(input);
				}
			}
		}
	}
}

That's it! Make sure to call the validate token method before the callout.

It will update the access token, and the named credential will automatically pick it up via the header formula.

Unit Testing

Another crucial thing is unit testing.

Unfortunately, you cannot test named credentials without seeing all data. Having see all data true is considered a bad practice, so it should be used with caution.

But in this case, we should be okay. Just make sure you do not access any other org data. Here is an example unit test for the class that I created.

@isTest(SeeAllData=true)
private class TokenServiceTestSuite {
  private static final String CREDENTIAL_NAME = 'Callout';

  @isTest
  static void testValidateAccessToken() {
    Test.setMock(HttpCalloutMock.class, new OAuthTokenResponseMock());
    Test.startTest();
    TokenService.validateAccessToken();
    Test.stopTest();
    ConnectApi.Credential cred = NamedCredentialUtils.getCredential(CREDENTIAL_NAME);
    Boolean tokenValid = NamedCredentialUtils.checkTokenValidity(CREDENTIAL_NAME);
    Assert.isTrue(tokenValid, 'The token is not valid');
  }

  public class OAuthTokenResponseMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(200);
        response.setHeader('Content-Type', 'application/json');
        TokenService.OAuthTokenResponse oauthTokenResponse = new TokenService.OAuthTokenResponse();
        oauthTokenResponse.access_token = 'test';
        oauthTokenResponse.expires_in = '3600';
        oauthTokenResponse.token_type = 'Bearer';
        response.setBody(JSON.serialize(oauthTokenResponse));
        return response;
    }
  }
}

Conclusion

In this post, I shared my experience dealing with the custom OAuth password flow using named credentials and how we could implement a persistent token.

The overall solution is straightforward, and it has been working nicely for me so far.

Hope you will find it helpful!

Nikita Verkhoshintcev photo

Nikita Verkhoshintcev

Freelance Salesforce Developer / Solution Architect

I am a senior Salesforce consultant based in Helsinki, Finland. I assist companies, consulting agencies, and ISV partners in building custom Salesforce applications. Since 2016, I have worked as a freelance full-stack developer and solution architect. I am always open to collaboration, so feel free to reach out!

Let's work together!

We help Salesforce customers and SI/ISV partners build custom Salesforce applications. Let's discuss how we can help you!

Contact us