Using JWT to share data between Orgs

Let’s suppose we want to show some records in our Salesforce Org, let’s call this Org “A”, but this information is stored in another Salesforce Org, “B”.

There are several ways to accomplish this requirement. We could use Salesforce-to-Salesforce to share records between different Orgs. We could also use third-party tools like Informatica Cloud or even creating our custom integration solution.

The goal of this article is to describe how we can take advantage of JWT (JSON Web Token) for implementing SSO between two Orgs.

In this example, we will retrieve the latest 10 accounts that have been created in our “B” Org from our “A” Org.

We need to set up several things in the Org that provides the information (“B”). First thing we need to do is to create a Connected App:

Please copy the Consumer key because you will need it later. We also have to create a Self-signed certificate under “Certificate and Key Management” section. After creating it, we need to export it because we will also need it later on.

And last, we are going to create a simple REST web service to expose our latest accounts:

@RestResource(urlMapping='/LastAccounts/*')
global with sharing class AccountRetriever{

@HttpGet
global static List getLastAccounts() {
return [SELECT Id, Name FROM Account ORDER BY createddate desc LIMIT 10];
}
}

That’s everything we have to do in the Org that provides the data. Let’s move then to the calling Org (“A”).

We need to create an Apex Class with the JWT implementation. There is a good example on force201 blog.

public class Jwt {

public class Configuration {
public String jwtUsername {get; set;}
public String jwtConnectedAppConsumerKey {get; set;}
public String jwtSigningCertificateName {get; set;}
public String jwtHostname {get; set;}
}

private class Header {
String alg;
Header(String alg) {
this.alg = alg;
}
}

private class Body {
String iss;
String prn;
String aud;
String exp;
Body(String iss, String prn, String aud, String exp) {
this.iss = iss;
this.prn = prn;
this.aud = aud;
this.exp = exp;
}
}

private class JwtException extends Exception {
}

private Configuration config;

public Jwt(Configuration config) {

this.config = config;
}

public String requestAccessToken() {

Map<String, String> m = new Map<String, String>();
m.put('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
m.put('assertion', createToken());

HttpRequest req = new HttpRequest();
req.setHeader('Content-Type','application/x-www-form-urlencoded');
req.setEndpoint('https://' + config.jwtHostname +'/services/oauth2/token');
req.setMethod('POST');
req.setTimeout(60 * 1000);
req.setBody(formEncode(m));

HttpResponse res = new Http().send(req);
if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
return extractJsonField(res.getBody(), 'access_token');
} else {
throw new JwtException(res.getBody());
}
}

private String formEncode(Map<String, String> m) {

String s = '';
for (String key : m.keySet()) {
if (s.length() > 0) {
s += '&';
}
s += key + '=' + EncodingUtil.urlEncode(m.get(key), 'UTF-8');
}
return s;
}

private String extractJsonField(String body, String field) {

JSONParser parser = JSON.createParser(body);
while (parser.nextToken() != null) {
if (parser.getCurrentToken() == JSONToken.FIELD_NAME
&& parser.getText() == field) {
parser.nextToken();
return parser.getText();
}
}
throw new JwtException(field + ' not found in response ' + body);
}

private String createToken() {

String alg = 'RS256';

String iss = config.jwtConnectedAppConsumerKey;
String prn = config.jwtUsername;
String aud = 'https://' + config.jwtHostname;
String exp = String.valueOf(System.currentTimeMillis() + 60 * 60 * 1000);

String headerJson = JSON.serialize(new Header(alg));
String bodyJson = JSON.serialize(new Body(iss, prn, aud, exp));

String token = base64UrlSafe(Blob.valueOf(headerJson))
+ '.' + base64UrlSafe(Blob.valueOf(bodyJson));
String signature = base64UrlSafe(Crypto.signWithCertificate(
'RSA-SHA256',
Blob.valueOf(token),
config.jwtSigningCertificateName
));
token += '.' + signature;

return token;
}

private String base64UrlSafe(Blob b) {

return EncodingUtil.base64Encode(b).replace('+', '-').replace('/', '_');
}
}

Next step is to import the certificate that we have exported previously. We have to go to the “Certificate and Key Management” section an use the “Import from keystore” button.

Finally, we need to add a couple of URLs in our Remote Site Settings configuration. In my case:

https://login.salesforce.com
https://eu3.salesforce.com
The first one is necessary to retrieve the token for calling the web service. Depending on our environment would be either login.salesforce.com or test.salesforce.com. The second one depends on our “B” Saleforce instance.

Everything is already in place. If we want to retrieve the latest accounts we could have the following code:

//Let's retrieve the access token
Jwt.Configuration config = new Jwt.Configuration();
config.jwtUsername = '*Username*';
config.jwtSigningCertificateName = 'jwt';
config.jwtHostname = 'login.salesforce.com';
config.jwtConnectedAppConsumerKey = '*consumerkey*';
String accessToken = new Jwt(config).requestAccessToken();
// Now, we can call the web service
HttpRequest req = new HttpRequest();
req.setMethod('GET'); req.setEndpoint('https://eu3.salesforce.com/services/apexrest/LastAccounts');
req.setHeader('Authorization', 'OAuth '+ accessToken);

Http http = new Http();

try {
HTTPResponse res = http.send(req);

//Helpful debug messages
System.debug('STATUS:'+ res.getStatus());
System.debug('STATUS_CODE:'+ res.getStatusCode());

String accounts = res.getBody();

} catch (System.CalloutException e) {
System.debug(e.getMessage());
}

Remember that the accounts string is in JSON format.

Leave a Reply

avatar
  Subscribe  
Notify of