22

I am new to use JWT for authenticating external requests. I went through this help article.

I am trying to implement above for the following usecase: External application (client) wants to access Salesforce Rest resource but instead of client_id/ client_secret wants to exchange JWT for accesstoken.

Steps Followed:

  • I created connected app and uploaded cert to verify signature

I have the following questions

  1. How will JWT identify a specified connected app? (I believe this is done as part of pre-authorization using another oAuth flow)
  2. What information in JWT is mandatory? Should Salesforce pass over client Id and client secret to external application so they can include this in JWT?
  3. Can I map already defined client_id from external application to initiate JWT outh flow?

In short, Authentication server provides JWT and that should provide access to Application Server (Salesforce) rest resources

Sander de Jong
  • 4,078
  • 8
  • 61
  • 114
sf_user
  • 2,240
  • 7
  • 35
  • 56
  • 2
    I've actually been working with the JWT flow for my current project. I'll get back to this when I get in to work (~30 minutes). – Derek F Dec 12 '17 at 12:54

1 Answers1

41

In the JWT Bearer OAuth flow, the connected app is identified by the connected app's consumer key (provided in the "iss" parameter of the JWT claims). Pre-authorizing users has very little to do with it (Profiles will need to be pre-authorized with the connected app, or users will need to approve the connected app through some other OAuth flow before you can successfully complete the flow and get your access token, but that is unrelated to the consumer key).

So far as I can tell, there are 4 items required in the JWT claims:

  • issuer "iss", this is the consumer key for your connected app
  • audience "aud", this is always https://login.salesforce.com for production, or https://test.salesforce.com for sandboxes (or whatever your Salesforce community url is, if you have one)
  • subject "sub", this is the username (user@yourcompany.tld) of the user you want to execute requests as
  • expiration "exp", this is the expiration time of the JWT itself, and provides a way to tolerate differences in client & server time. This is the unix timestamp (seconds since unix epoch) + a little more time to allow for the JWT to make it to Salesforce. Really, any timestamp 1 minute or more in the future should work fine here1. It has nothing to do with how long the access token is valid for.

The client secret is not used at any point in the JWT Bearer flow.

Instead, that's why you create a certificate to use. If you created a certificate in Salesforce to use for this purpose, you'll need to download the individual certificate (not export to a keystore, you should get a .crt file out of it). You'll also need to make sure that your connected app has "use digital signatures" checked (after which, when editing your connected app, you can upload the .crt file).

The permissions (Oauth scopes) that your connected app requires for the JWT Bearer flow to work are:

  • Access and manage your data (api)
  • Perform requests on your behalf at any time (refresh_token, offline_access)

A note about session timeout:

Once you get an access token, it is treated just like any other session in Salesforce. If you make a request every so often2, the same access token will remain valid. If you go to your connected app and click the "manage" button, and then click "edit policies" you can adjust the session timeout independent of your org's session timeout. If you don't explicitly specify a session timeout in your connected app, it'll just use your org's default session timeout.

As for your third question, I'm not quite sure how to answer that. It doesn't sound (to me) like you need that information at all. Everything you need to know (besides the "expr") should generally be static, and information available in or related to Salesforce.

To help out a bit more, I'll go through the steps required to manually construct the JWT in anonymous apex. It's covered in the help article that you linked in your question, but the wording in that help article was a bit on the obtuse side, I thought.

// The consumer key for a connected app of mine
String iss = '3MVG9jfQT7vUue.EIXJ6Vbqu4LHxslR9fX0MHAp1SyQhCTocvhaXPT9eWuD7kxHoHsXPIiTjMQKv2Ln<last couple of characters removed';

// I'm doing this in a sandbox, so aud is test.salesforce.com // Replace with login.salesforce.com for production environments String aud = 'https://test.salesforce.com';

// A user that belongs to one of the pre-authorized profiles for your connected app. // Setup -> Create -> Apps // Connected apps are at the bottom of the page (at time of writing, API v41.0) // Go into your target connected app and click the "manage" button // Click "Manage Profiles" near the bottom of the page, and add/remove profiles as needed. String sub = 'derek@myCompany.com';

// Expiration time of the JWT itself // Best to make this a long instead of an int // Adding 5 minutes is arbitrary, anything less than a minute is fairly dangerous, // and anything more is fine (could be minutes, days, months, or years in the future) Long exp = DateTime.now().addMinutes(5).getTime();

// Start constructing the header and claims // The "alg" will pretty much always be "RS256" with Salesforce String jwtHeader = '{"typ":"JWT","alg":"RS256"}'; String jwtClaims = '{"iss":"' + iss + '","sub":"' + sub + '","aud":"' + aud + '","exp":' + exp + '}';

// Now we have to start Base64 encoding things. // For JWT, Base64 is not good enough, there are 2 characters that are not URL-safe // which we need to deal with. // '+' needs to be replaced with '-', and '/' needs to be replaced with '' // This variant of Base64 is called Base64Url, and Salesforce doesn't provide us // with a method to do that automatically. // This step takes the JWT header and claims, separately Base64Url encodes them, and // concatenates them with a period/full stop String jwtRequest = System.encodingUtil.base64Encode(Blob.valueOf(jwtHeader)).replace('+', '-').replace('/', '') + '.' + System.encodingUtil.base64Encode(Blob.valueOf(jwtClaims)).replace('+', '-').replace('/', '_');

// Here is what the certificate is used for. // We sign the Base64Url-encoded JWT request with the private key of the certificate // If you have the certificate stored in Salesforce (Setup, quick find box, type // certificate and key management), then you can use Crypto.signWithCertificate(), // providing the unique name of the certificate you want to use String signature = System.encodingUtil.base64Encode(Crypto.signWithCertificate('RSA-SHA256', Blob.valueOf(jwtRequest), 'My_Cert')).replace('+', '-').replace('/', '_');

// Otherwise, if you have the private key of the certificate, you can use Crypto.sign() //String signature = System.encodingUtil.base64Encode(Crypto.sign('RSA-SHA256', Blob.valueOf(jwtRequest), '<long string, the private key of the cert>')).replace('+', '-').replace('/', '_');

// One of the final steps, append the jwt request and the signature of that request // (again with a period/full stop between them) String signedJwtRequest = jwtRequest + '.' + signature;

// The JWT is fully constructed, now it's time to make the call to get the access token.

String payload = 'grant_type=' + System.EncodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:jwt-bearer', 'UTF-8'); payload += '&assertion=' + signedJwtRequest;

Http httpObj = new Http(); HttpRequest req = new HttpRequest(); HttpResponse res;

// My sandbox is on cs52 for the moment // You'll need to replace this with your sandbox pod (or production pod, or your custom // domain if you've embraced Lightning Experience) // Having a "My Domain" set up for production is very helpful, as Salesforce can // eventually migrate your production org to a different pod. // If you're doing this in anonymous apex (or in Apex in general), don't forget to // add this domain to your remote site settings. // No matter what environment you're using, the tail end of the endpoint you'll // be using to submit the JWT is '/services/oauth2/token' req.setEndpoint('https://cs52.salesforce.com/services/oauth2/token'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); req.setBody(payload);

res = httpObj.send(req);

// If everything goes well, res.getBody() should contain JSON with "access_token"

Without the comments (to make it shorter/easier to read)

String iss = '3MVG9jfQT7vUue.EIXJ6Vbqu4LHxslR9fX0MHAp1SyQhCTocvhaXPT9eWuD7kxHoHsXPIiTjMQKv2Ln<last couple of characters removed';
String aud = 'https://test.salesforce.com';
String sub = 'derek@myCompany.com';
Long exp = DateTime.now().addMinutes(5).getTime();

String jwtHeader = '{"typ":"JWT","alg":"RS256"}'; String jwtClaims = '{"iss":"' + iss + '","sub":"' + sub + '","aud":"' + aud + '","exp":' + exp + '}';

String jwtRequest = System.encodingUtil.base64Encode(Blob.valueOf(jwtHeader)).replace('+', '-').replace('/', '') + '.' + System.encodingUtil.base64Encode(Blob.valueOf(jwtClaims)).replace('+', '-').replace('/', '');

String signature = System.encodingUtil.base64Encode(Crypto.signWithCertificate('RSA-SHA256', Blob.valueOf(jwtRequest), 'My_Cert')).replace('+', '-').replace('/', '_');

// Otherwise, if you have the private key of the certificate, you can use Crypto.sign() //String signature = System.encodingUtil.base64Encode(Crypto.sign('RSA-SHA256', Blob.valueOf(jwtRequest), '<long string, the private key of the cert>')).replace('+', '-').replace('/', '_');

String signedJwtRequest = jwtRequest + '.' + signature;

String payload = 'grant_type=' + System.EncodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:jwt-bearer', 'UTF-8'); payload += '&assertion=' + signedJwtRequest;

Http httpObj = new Http(); HttpRequest req = new HttpRequest(); HttpResponse res;

req.setEndpoint('https://cs52.salesforce.com/services/oauth2/token'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); req.setBody(payload);

res = httpObj.send(req);

Of course, if you're going to be doing this from within Salesforce (the JWT flow is an excellent choice for making an integration between two orgs), there is an easier way, the JWS, JWT, and JWTBearerTokenExchange classes in the Auth namespace

// 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.setAud(aud);
jwt.setIss(iss);
jwt.setSub(sub);
jwt.setValidityLength(Integer.valueof(exp));

// Storing the certificate in Salesforce is a requirement for using the JWS class Auth.JWS jws = new Auth.JWS(jwt, 'My_Cert_Name');

String tokenEndpoint = 'https://cs52.salesforce.com/services/oauth2/token'; Auth.JWTBearerTokenExchange bearer = new Auth.JWTBearerTokenExchange(tokenEndpoint, jws); String accessToken = bearer.getAccessToken();

1: Salesforce actually already accounts for this, and allows a deviation of up to 3 minutes in the expiry time as evidenced in https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5
The date and time at which the token expires, expressed as the number of seconds from 1970-01-01T0:0:0Z measured in UTC. Salesforce allows a 3-minute buffer for clock skew.
So this shouldn't be necessary, but it is one of the first things to try if the JWT flow isn't working as expected

2: Empirical evidence suggests that if you make any request in the second half of the session timeout, then the session timeout is recalculated/extended

Derek F
  • 61,401
  • 15
  • 50
  • 97
  • To Summarize: (If external application wants to invoke SF Rest resource). Please correct me if I go wrong anywhere
    1. Create Connected App (Pass over Client Id, url, useremail who has access to connected app so they have to use these in "iss", "aud", "sub" respectively).
    2. Get certificate from external application and upload in digital signature in connected app
    3. Pre-authorize/ validate the app using oAuth flow for the user email address mentioned above (One -Time)
    4. External application calls "../services/oauth2/token" to retrieve accesstoken which can be used in subsequent requests.
    – sf_user Dec 12 '17 at 14:51
  • 2
    @sf_user Almost. The user email that you give to your external application just has to be any active user in your org who belongs to a profile that you pre-authorize. Pre-authorizing profiles does not involve any OAuth things (it's declaratively managed inside of Salesforce). The only time that you need to use another OAuth flow is if your Connected App's policy is set to "Users may self-authorize". One of the big benefits of the JWT Bearer flow is that it can be handled without any user interaction, so using "Users may self-authorize" defeats a bit of the purpose here. – Derek F Dec 12 '17 at 14:57
  • I see from the documentation here[1] The issuer (iss) must be the OAuth client_id or the connected app for which the developer registered their certificate. Does this mean external apps can use their own client_id do get access into salesforce ? If yes, how to recognize this user as they will not be using connected app's client_id. Also, are "iss" and "client_id" exchangeable ? meaning that instead of iss can I use "client_id" to pass client id ?

    [1] https://help.salesforce.com/articleView?id=remoteaccess_oauth_jwt_flow.htm&type=5

    – sf_user Jan 17 '18 at 17:31
  • 1
    @sf_user This is something that would be worthy of another question. Comments are generally best thought of as temporary. – Derek F Jan 17 '18 at 17:34
  • @Derek What library did you use for this part of the code: Http httpObj = new Http(); HttpRequest req = new HttpRequest(); I couldn't find, in Java, nothing that was quite like that nor that have a "req.setBody(payload);" method. Thank you ! – JokerPW Feb 09 '18 at 19:17
  • 1
    @JokerPW I didn't use any library (Apex doesn't really have a concept of "a library"). My example was in Apex, and the Http, HttpRequest and HttpResponse classes are part of the System namespace – Derek F Feb 09 '18 at 20:08
  • @DerekF I am trying to do a similar setup and can you please let me know what the significance of the callback url in the JWT scenario ? What should it be ? – HSG Jul 20 '18 at 14:51
  • @HSG Not entirely sure off the top of my head, but this really sounds like it should be a separate question (after you see if the documentation has anything specific to say about that, of course). – Derek F Jul 20 '18 at 15:28
  • Thanks @DerekF. I was just curious if the callback url plays any role or not. I got everythign to work on my end. Thanks for your answers earlier. – HSG Jul 20 '18 at 16:24
  • Hi, thanks for a good answer @DerekF Two questions:
    • In the REST API developer guide it says that supported oauth flows are web server flow, user-agent flow and username-password flow. Do you know the reason why JWT Bearer token flow is not listed here? Can I assume that after authentication with the JWT flow I can make following REST or SOAP calls using the access token received?
    • Which is the user that will be used as the createdBy or LastModifiedBy user of records for following update/insert calls?

    Thanks

    – nickwick76 Nov 24 '18 at 10:01
  • Also for the ones interested, here's a short walk-through I found on how to setup a test JWT Bearer Token Flow -> https://gist.github.com/booleangate/30d345ecf0617db0ea19c54c7a44d06f – nickwick76 Nov 24 '18 at 10:11
  • @nickwick76 That sounds like it's worthy of its own question (or maybe several questions) – Derek F Nov 24 '18 at 14:16
  • @DerekF My understanding is : the client who is requesting the access token should generate the certificate and associated private key. Please correct if wrong? – Cloud Man Jul 15 '19 at 00:55
  • 1
    Hi @DerekF, I am trying this way. But, I got this exception in Apex class. Can you please help me? System.NoDataFoundException: Data Not Available: The data you were trying to access could not be found. It may be due to another user deleting the data or a system error. If you know the data is not deleted but cannot access it, please look at our page. – Subash Chandrabose Jul 25 '19 at 17:14
  • @MSCB You should make your own question, and include your code (and the full error + stack trace) in it. Including a reference to this question might help other people (aside from me) understand what you're trying to do and be able to offer other insights as well. – Derek F Jul 25 '19 at 17:18
  • @SFNinja You should ask your own question for that. – Derek F Jul 25 '19 at 17:25
  • @DerekF : One quick question, if we set the Audience as partner community url, then can we set "sub" as a partner user and will it work for getting JWT token ? – AjaySFDC Nov 09 '22 at 17:15
  • @AjaySFDC No idea, and that should be a separate question. – Derek F Nov 10 '22 at 14:15
  • Thanks @DerekF I got this working for Partner community URL and partner user. But I thought i can test this via Postman.. But only i could do it via code. Postman uses only CryptoJs and CryptoJs doesnt support RSASHA256 format. Could you please suggest some other ways to test this without coding ? – AjaySFDC Nov 11 '22 at 21:07
  • One comment about a line in the code: the line after the comment "Otherwise, if you have the private key of the certificate, you can use Crypto.sign()". doesn't work because it can't find an overload with params of type (string, blob, string). I added the below before the line after the comment:

    Blob privateKey = System.encodingUtil.base64Decode('');

    I replaced the 3rd parameter with privateKey:

    String signature = System.encodingUtil.base64Encode(Crypto.sign('RSA-SHA256', Blob.valueOf(jwtRequest), privateKey)).replace('+', '-').replace('/', '_');

    – Jeff Dec 27 '22 at 23:21
  • Does this work with a developer ORG? I am trying to set this up, hitting my developer org URL and getting a grant not available error. – digidigo Mar 04 '23 at 22:15