5

I can make requests with Bearer from Auth.JWT:

Auth.JWT jwt = new Auth.JWT();
jwt.setSub('[email protected]'); 
jwt.setAud('https://accounts.google.com/o/oauth2/token'); 
jwt.setIss('[email protected]');

Map<String, Object> claims = new Map<String, Object>();
claims.put('scope', 'https://www.googleapis.com/auth/bigquery');
jwt.setAdditionalClaims(claims);
Auth.JWS jws = new Auth.JWS(jwt, 'privatekey');
String token = jws.getCompactSerialization();
String tokenEndpoint = 'https://accounts.google.com/o/oauth2/token';
Auth.JWTBearerTokenExchange bearer = new Auth.JWTBearerTokenExchange(tokenEndpoint, jws);
String accessToken = bearer.getAccessToken();

But if I copied all fields to Named Credentials:

sf

and try to make request:

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:GoogleCloud/projects/service-portal-rlj/queries');
req.setMethod('POST');
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());

I receive error:

System.CalloutException: Unable to complete the JWT token exchange.

2
  • Just very recently, I have come across similar issue with NC-JWT Token Exchange. That is, the parameters work with Apex's Auth.JWT, but it fails on NC-JWT Token Exchange. I get: "System.CalloutException: Unable to complete the JWT token exchange. Error: invalid_scope. Error description: Invalid OAuth scope or ID token audience provided.." I tried snooping on the payload it sends and it turned out that the "scope" was being passed by the NC as a separate parameter in the body, rather than part of the JWT assertion.
    – pnoytechie
    Sep 1, 2022 at 23:20
  • @pnoytechie This Q&A is specifically for Google Cloud. GCP does not have scope in the token: cloud.google.com/iap/docs/signed-headers-howto
    – identigral
    Sep 5, 2022 at 17:40

3 Answers 3

5

GCP also allows JWT as a bearer token (no exchange). On paper, Named Credential supports this option. Unfortunately since this is a proprietary option that is not standardized by a published RFC, the token payload varies from service to service. In case of GCP, it wants a key id (kid) in the token header and Named Credential can't cope with that.

Therefore building a JWT in Apex and exchanging it for access token or sending it as a bearer token is the way to go.

UPDATE (Aug 2021): talked to Prod Mgmt about this. Their response is that this works as designed and that it was designed primarily for interoperability with Mulesoft. In Spring '22 (Safe Harbor) they plan on providing a flexible backend data model for Named Credentials that will handle all flavors of claims.

3
  • The "kid" portion appears to be addressed now within GCP Identity Federation but the issue with the "subject_token" on the SF side prevents to from having a complete solution. At least that's my current understanding.
    – jldupont
    Jun 13, 2022 at 18:54
  • @jdupont The "kid" portion appears to be addressed now within GCP Identity Federation - not sure what you're looking at but GCP still requires kid.
    – identigral
    Jun 13, 2022 at 19:50
  • Sorry, I had encountered a limitation some time ago (can't recall exactly what now :-( . On the GCP side, it expected to have the "kid" parameter set to the client ID or something along those lines and SF did not provide this.
    – jldupont
    Jun 13, 2022 at 20:03
4

To build off of identigral's answer, I also ran into this and had to just build the JWT IN Apex.

I did open up a case with SF support about the JWT token exchange and they said: "We only support Registered claims. We do not support custom Public or Private claims as of yet." They also mentioned that they are "...working on adding public/private claim support to Named Credentials JWT. But unfortunately, we do not have any ETA on this."

So probably not going to happen any time soon but at least we know they are aware of it and are working on it.

2

This currently works with named credentials / external credentials. The only reason you would stick with Auth.JWT is if you are using domain wide delegation and need to dynamically change the user you are impersonating.

I recently had to set this up and could find nothing online providing clear directions how to do it, most (like the existing answers here) say it cannot be done and to use Auth.JWT. I am adding this in the hope it saves someone an hour or two of going around in circles in the future.

The instructions below assume you are creating a server to server connection between SF and Google, to allow your system to interact with googleapis with no permission required from the running user.

Step 1:

Import a certificate to SF from a JKS keystore, you will need to create the keystore from a .p12 format key that you export from your service account in GCP. Once you have the p12, the keytool command probably looks like this

keytool -importkeystore -srckeystore ./serviceaccountfilename.p12 -destkeystore destination_jks_filename.jks -srcstorepass notasecret -srcalias privatekey -srcstoretype pkcs12 -deststoretype jks -destalias set_your_destination_cert_alias -deststorepass notasecret

note: Before you can import any .jks you must first have enabled an identity provider in salesforce. If you haven't done this, the easy way is to create a certificate (in the sf UI) and add an identity provider using the certificate you just created.

Step 2:

Create an External credential with the following settings;

Common Claims;

  • Issuer (iss): <your_service_account>
  • Subject (sub): <your_service_account> (use a different user email here if you have enabled domain wide delegation and wish to impersonate a user)
  • Audience (aud): https://oauth2.googleapis.com/token
  • JWT Expiration (Seconds): 3600 (or whatever is appropriate for your use case)

JWT Signing

  • Signing Certificate: <your_uploaded_cert>
  • Signing Algorithm: RS256

The trick is to add your scope to the claims body. Once you have saved the record scroll down to the claims section and click the 'edit' button, when the modal opens click the 'add' button to add a claim with the following params;

  • Name: scope
  • Value: <space_separated_list_of_scopes_required>
  • Type: JWT Body Claim

The scopes requested here must match the access defined for your service account (and domain wide delegation if you are using it)

To allow your users to access your named credentials you should add a principal record with the following settings;

  • Parameter Name: <your_choice>
  • Sequence Number: <your_choice>
  • Identity Type: Named Principal
  • Scope: LEAVE THIS BLANK

If you add a scope at this point and use domain wide delegation you will get an error stating "Invalid downscoping, scopes should not be specified as a request parameter"

You will also need a permission set providing access to the principal and assign it to all users who will run the process.

Step 3:

Create a named credential using the following;

Authentication

  • External Credential: <Select_your_external_credential>

Callout Options

  • Generate Authorization Header: true

Make a note of the named credential api name, you need this for the callout.

Step 4:

You can now callout to google api using your service account like this

// get a list of calendars belonging to the user specified in subject (sub) claim of the external credential 
// (use the service account address to access the service account itself)

HttpRequest request = new HttpRequest();
request.setEndpoint('callout:named_credential_api_name/users/me/calendarList/primary');
request.setMethod('GET');

Http http = new Http();
try {
    HTTPResponse response = http.send(request);
    System.debug('Response Status: ' + response.getStatus());
    System.debug('Response Body: ' + response.getBody());
} catch(System.CalloutException e) {
    System.debug('Callout error: ' + e.getMessage());
}

Things to note:

  1. If you just created your service account and get a list of calendars, expect it to be empty
  2. You must create and manage the service account resources using the API, you cannot do it from the UI.
  3. Depending on your environment you may prefer to use the google sdk client libraries (node, python or java) from admin/setup tasks on the service account
  4. You should test your service account using the client libraries if you have any permission issues to ensure the account is set up correctly at google's end
6
  • This is now possible because Google no longer requires key id in the JWT header for server-to-server oAuth. From their doc: The key ID is optional and if an incorrect Key ID is specified GCP will try all keys associated with the service account to verify the token and reject the token if no valid key is found But Google being Google, they caveat this: Google reserves the right to reject tokens with incorrect key IDs in the future. So you can roll without kid but we don't recommend that.
    – identigral
    Feb 26 at 2:14
  • Did a brief test - Out of the box, External Cred sets kid to the name of the keypair (aka the cert in Certs and Key Mgmt). That will fly as long as Google doesn't enforce verification. BUT thankfully the kid claim is editable! Change its value be the ID of the private key attached to the service account and you're safe forever and ever (or until Google changes their mind again)
    – identigral
    Feb 26 at 4:09
  • 1
    @identigral I tried with and without kid set - it worked without so i left it off Feb 27 at 1:32
  • I'm annoyed with myself because I had this problem a fortnight or so ago, used this to solve it, and now I have a new scratch org and am missing something in the setup because I can't get it working again. I can't recall how I sorted this out last time, but the P12 I get from Google has a password with 10 characters, and the Salesforce docs on converting a P12 to a JKS say it can only handle 6-8 characters. But I got it working last time!
    – dyson
    Apr 15 at 21:23
  • @dyson the command line above for using the keytool has the switch '-deststorepass notasecret'. Does this match what you used? Apr 16 at 22:20

You must log in to answer this question.

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