3

GOAL: Retrieve an access token from a 3rd party service so as to make a call with this token to 3rd party API. This is the quote from the doc.

The ExternalApp API is accessible using an Oauth2 JWT obtained from ExternalApp auth service following Oauth2 Client Credentials flow. More simply, this is an exchange of the client credentials for a limited lifespan token that can be used for authentication and authorization.

SANITY TEST: I used cURL (see below) and got a token that worked for GET calls to the endpoint. (ID and secret removed)

curl --location --request POST "https://auth-stage.ExternalApp.org/oauth/token" 
--header "Accept: application/json" 
--header "Content-Type: application/x-www-form-urlencoded" 
--data-urlencode "grant_type=client_credentials" 
--data-urlencode "scope=create:loan_draft read:loans" 
--data-urlencode "audience=https://partner-api-stage.ExternalApp.org" 
--data-urlencode "client_id=<client ID>" 
--data-urlencode "client_secret=<client secret from Partner Admin>"

the response was good! (the access token below is not real)

{"access_token":"yJhbGciXXXUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjFublhjRFRHIn0.eyJhdWQiOlsiaHR0cHM6Ly9wYXJ0bmVyLWFwaS5rMS5raXZhLm9yZyJdLCJzY29wZSI6WyJjcmVhdGU6bG9hbl9kcmFmdCIsInJlYWQ6bG9hbnMiXSwiaXNzIjoiaHR0cHM6Ly9hdXRoLmsxLmtpdmEub3JnLyIsInBhcnRuZXJJZCI6IjEiLCJleHAiOjE2MDIxNTY2MTgsImp0aSI6IlJVc2l2VVhHZ2hoeC1Zdjl6emEzZ2daaTZhbXXXImNsaWVudF9pZCI6IlFEMmxPRzZMbTN2RWQ5QTZEdVh3eFJWOE1OMEp6cDVreSJ9.U_tCMX5ra7Q0NFwr1FKlgqCBEmlprY-PuXXX6bNzEREtJABh0hBr-zEKXQEhHYTpHjjNquOHK7Q8hnQ30IVVhE6jXUO8_OgRfmczlQ8sDkRzmx5PTc99my0bs6zn8owRfEEwBGJcvNt_oT8iRASnlij99d7dozTFguBnT7_hauXoq2C4DFmRx3rjfnCbI9G7Ue_4Gh3jnF7VYI9HefLvYHBCS0SP3a-QqNuR5w1itRevj8KOIhC5lKuJn22cRXW9PQL3G9XGyK0h8sFZj7blhLETMLFAHbrWFUGzawEBAeLQbQhvvu78dp0RzgY0OvS2XXzTgxpg0TcgsrWuDdjFAA",
"token_type":"bearer",
"expires_in":43199,
"scope":"create:loan_draft read:loans",
"iss":"https://auth-stage.ExternalApp.org/",
"partnerId":"468",
"jti":"erBXqikrdZ8QP1Jlc1HyjoJyoGQ"}

APPROACH #1: I tried to make a Named Credential

enter image description here

and then ran this APEX anonymously

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:ExternalApp_Stage/some_path');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());

Result of approach #1 Error: System.CalloutException: Unable to complete the JWT token exchange.

This is I guess not too surprising, since there was no place in the Named Credential for me to put the client id, Client secret, grant type... also I can't set the header... but I read it is taken care of by Salesforce.

APPROACH #2: all out APEX

i was trying to learn from https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_Auth_JWTBearerTokenExchange.htm#apex_Auth_JWTBearerTokenExchange_constructors

// aud, iss, sub, and exp still need to be specified, but these Auth classes
//   take care of everything else for you.

        Auth.JWT jwt = new Auth.JWT();
        jwt.setSub('[email protected]'); 
        jwt.setAud('https://partner-api-stage.ExternalApp.org'); 
        jwt.setIss('ClientSecret+++1BYjySfmsPhT/eWCTy40r1PgtDkpNWGL7NR/6MEeCfuO95tSfAb');
        jwt.setValidityLength(36000);

        //Additional claims to set scope
        Map<String, Object> claims = new Map<String, Object>();
        claims.put('scope', 'create:loan_draft read:loans');
        claims.put('client_id', '<client id>');
        claims.put('client_secret', 'ClientSecret+++1BYjySfmsPhT/eWCTy40r1PgtDkpNWGL7NR/6MEeCfuO95tSfAd');
        
        jwt.setAdditionalClaims(claims);

        //Create the object that signs the JWT bearer token
        Auth.JWS jws = new Auth.JWS(jwt, 'Cert2021July9');
        
        //Get the resulting JWS in case debugging is required
        String token = jws.getCompactSerialization();
        
        //Set the token endpoint that the JWT bearer token is posted to
        String tokenEndpoint = 'https://auth-stage.ExternalApp.org/oauth/token';
        
        //POST the JWT bearer token
        Auth.JWTBearerTokenExchange bearer = new Auth.JWTBearerTokenExchange();
        bearer.setTokenEndpoint(tokenEndpoint);
        bearer.setJWS(jws);
        bearer.setGrantType('client_credentials');

        
        //Get the access token
        String accessToken = bearer.getAccessToken();

result of approach #2 Error - i probably messed up the iss, Aud, Sub parameters... :(

Error 401: <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Wed Sep 15 06:36:11 GMT 2021</div><div>There was an unexpected error (type=Unauthorized, status=401).</div><div></div></body></html>

2
  • The phrase Oauth2 JWT obtained from ExternalApp auth service following Oauth2 Client Credentials flow refers to the format of the token returned by ExternalApp auth service. It does not imply JWT Bearer Flow in oAuth. You want Client Credentials flow. Unfortunately Named Credential does not support this out of the box, you need Apex. Examples: one two
    – identigral
    Sep 15, 2021 at 15:42
  • Thank you so much for cleaning up the questions @identigral and offering APEX examples, I will review them now :)
    – Gordon Lau
    Sep 15, 2021 at 15:43

1 Answer 1

1

Thanks to @identigral to pointing to the following answers http callout `Client credentials are missing` by using Basic Auth

What I learned was:

  • need to make a blob with ClientId:ClientSecret and encode it for the Authorization Header
  • Scopes and other claims need to get into the body, separated by ampersand

My next challenge will be find a way to store the access token so it can be used in other calls and not just do this again and again.

For now though The Anonymous APEX that ended up working was this:

        // Set variables to form the initial JWT Client Credential flow 

        // Warning to SELF: bad form to put Client ID and Secret in plain text in code, consider Static Resources or Custom Variables.
        string ClientId = 'YXXXXXmkqR0ED7xLuBj74p9BXynOadz6l';
        string ClientSecret = 'F2ciG4+++XXXXXXXXXXXXX/eWCTy40r1PgtDkpNWGL7NR/6MEeCfuO95tSfAb';
        string endpoint = 'https://auth-stage.ExternalApp.org/oauth/token';
        string body = 'grant_type=client_credentials&audience=https://partner-api-stage.ExternalApp.org&scope=create:loan_draft read:loans';

        // variables to store token response if we are successful
        string AccessToken = '';
        string AccessScope = '';
        string AccessIss = '';
        string PartnerId = '';
        string TokenJti = '';

        Blob headerValue = Blob.valueOf(ClientId + ':' + ClientSecret);
        String authorizationHeader = 'Basic ' +  EncodingUtil.base64Encode(headerValue);
        system.debug('Encoder Value' + EncodingUtil.base64Encode(headerValue));

        // Construct the request
        http http = new http();
        httpRequest request = new httpRequest();
        request.setMethod('POST');
        request.setEndpoint(endpoint);
        request.setHeader('Accept', 'application/json');
        request.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        request.setHeader('Authorization', authorizationHeader);
        request.setBody(body);

        // Send the request
        httpResponse Res = http.send(request);
        system.debug('response status code ' + Res.getStatusCode() );


        if (Res.getStatusCode() == 200) {
            // Deserializes the JSON string into collections of primitive data types.
            Map<String, Object> ResponseMap = (Map<String, Object>) JSON.deserializeUntyped(Res.getBody());
            system.debug('response OK ' + Res.getBody() );
            
            AccessToken = (string)ResponseMap.get('access_token');
            system.debug('access token: ' + AccessToken );
            
            AccessScope = (string)ResponseMap.get('scope');
            system.debug('access Scope: ' + AccessScope );
            
            AccessIss = (string)ResponseMap.get('iss');
            system.debug('Issuer: ' + AccessIss );
            
            PartnerId = (string)ResponseMap.get('partnerId');
            system.debug('Partner ID: ' + PartnerId );
            
            TokenJti = (string)ResponseMap.get('jti');
            system.debug('Token jti: ' + TokenJti );
            
        } else {
            system.debug('response error ' + Res.getBody() );
        }        

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .