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