21

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

1
  • 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, 2017 at 12:54

1 Answer 1

40

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 (u[email protected]) 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 = '[email protected]';

// 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 = '[email protected]';
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

21
  • 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, 2017 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, 2017 at 14:57
  • 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, 2018 at 17:34
  • 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 9, 2018 at 20:08
  • 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.
    – MSCB
    Jul 25, 2019 at 17:14

You must log in to answer this question.

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