How JWT Works in Salesforce?

on

|

views

and

comments

Hey everyone,

welcome back, in this post we are going to learn how to implement the JWT authentication using Apex in Salesforce.

Sometimes you want to authorize servers to access data without interactively logging in each time the servers exchange information. For these cases, you can use the OAuth 2.0 JSON Web Token (JWT) bearer flow. This flow uses a certificate to sign the JWT request and doesn’t require explicit user interaction. However, this flow does require prior approval of the client app.

When we talk about JSON Web Token, it is consist of 3 parts

  1. Headers – Which contains the algorithm which will be used to sign the request
  2. Payload – The actual data and information about the users
  3. Signature – Signature consists of 3 parts and the structure is given below.
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  Private Key
)

In this blog post, we are going to authenticate Einstein Platform Services and you can get your account from here. Sign up using your salesforce account and download the key which we will be used for authentication purposes.

JWT can be authenticated using 2 ways

  • Using Private Secret key – The platform which we are trying to access will provide the Key.
  • Using Certification

let’s start preparing all three parts one by one

Step 1 – Prepare header

As we know that header contains the algorithm. So it will look like below

{“alg”:”RS256″}

Step 2 – Prepare Payload

Payload consist of 4 parts

  1. iss – The issuer must contain the OAuth client_id or the connected app for which you registered the certificate. and in our case it is developer.force.com
  2. aud – The audience identifies the authorization server as an intended audience. For Example, https://login.salesforce.comhttps://test.salesforce.com, or https://community.force.com/customers if implementing for a community. or https://api.einstein.ai/v2/oauth2/token for einstein API
  3. sub- is either email of the user or target username
  4. exp – The validity must be the expiration time of the assertion within 3 minutes, expressed as the number of seconds from 1970-01-01T0:0:0Z measured in UTC.

Here is the Example

{
  "iss": "developer.force.com",
  "sub": "my@email.com",
  "aud": "https://api.einstein.ai/v2/oauth2/token",
  "exp": "1333685628"
}

Now, once we have prepared the payload, we need to url encode this as base64.

After we are done with the encoding the string now Create a string for the Encoded JWT Header and the encoded JWT Claims Set in this format. Like below

encoded_JWT_Header + "." + encoded_JWT_Claims_Set

Step 3 – Sign the result from Step 2 using Private Key or cert

encoded_JWT_Header + "." + encoded_JWT_Claims_Set

Now, as we have prepared all three parts of JWT the final step is to get the access token. To get the access token we need to make the post request. Below is sample request.

POST /services/oauth2/token HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded

grant_type= urn:ietf:params:oauth:grant-type:jwt-bearer&
assertion=eyJpc3MiOiAiM01WRz...[omitted for brevity]...ZT

Include these parameters in the post.

PARAMETER DESCRIPTION
grant_type Use these values for the grant type: urn:ietf:params:oauth:grant-type:jwt-bearer.
assertion The assertion is the entire JWT value.

To prepare the JSON we are using JSON class and JSONGenerator which are provided by salesforce.

Remember, we signed up for the Einstein Platform Account and downloaded the key file. Upload that key files under files inside salesforce.

Below is the complete code for the authentication.

JWT Class to prepare the JWT header, payload.

public class JWT {
    
    public String alg {get;set;}
    public String iss {get;set;}
    public String sub {get;set;}
    public String aud {get;set;}
    public String exp {get;set;}
    public String iat {get;set;}
    public Map<String,String> claims {get;set;}
    public Integer validFor {get;set;}
    public String cert {get;set;}
    public String pkcs8 {get;set;}
    public String privateKey {get;set;}
    
    public static final String HS256 = 'HS256';
    public static final String RS256 = 'RS256';
    public static final String NONE = 'none';
    
    public JWT(String alg) {
        this.alg = alg;
        this.validFor = 3600;
    }
    
    public String assertion() {
        
        String jwt = '';
        JSONGenerator header = JSON.createGenerator(false);
        header.writeStartObject();
        header.writeStringField('alg', this.alg);
        header.writeEndObject();
        String encodedHeader = base64URLencode(Blob.valueOf(header.getAsString()));
        
        JSONGenerator body = JSON.createGenerator(false);
        body.writeStartObject();
        body.writeStringField('iss', this.iss);
        body.writeStringField('sub', this.sub);
        body.writeStringField('aud', this.aud);
        Long rightNow = (dateTime.now().getTime()/1000)+1;
        body.writeNumberField('iat', rightNow);
        body.writeNumberField('exp', (rightNow + validFor));
        if (claims != null) {
            for (String claim : claims.keySet()) {
                body.writeStringField(claim, claims.get(claim));
            }
        }
        body.writeEndObject();
        jwt = encodedHeader + '.' + base64URLencode(Blob.valueOf(body.getAsString()));
        
        if ( this.alg == HS256 ) {
            Blob key = EncodingUtil.base64Decode(privateKey);
            Blob signature = Crypto.generateMac('hmacSHA256',Blob.valueof(jwt),key);
            jwt += '.' + base64URLencode(signature);  
        } else if ( this.alg == RS256 ) {
            Blob signature = null;
            
            if (cert != null ) {
                signature = Crypto.signWithCertificate('rsa-sha256', Blob.valueOf(jwt), cert);
            } else {
                Blob privateKey = EncodingUtil.base64Decode(pkcs8);
                signature = Crypto.sign('rsa-sha256', Blob.valueOf(jwt), privateKey);
            }
            jwt += '.' + base64URLencode(signature);  
        } else if ( this.alg == NONE ) {
            jwt += '.';
        }
        return jwt;
    }
    
    public String base64URLencode(Blob input){ 
        String output = encodingUtil.base64Encode(input);
        output = output.replace('+', '-');
        output = output.replace('/', '_');
        while ( output.endsWith('=')){
            output = output.subString(0,output.length()-1);
        }
        return output;
    }
}

JWTBearerFlow Class for getting the Access Token

public class JWTBearerFlow {
    
    public static String getAccessToken(String tokenEndpoint, JWT jwt) {
        
        String grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
        String access_token = null;
        String body = 'grant_type='+EncodingUtil.urlEncode(grantType, 'UTF-8')+'&assertion=' + jwt.assertion();
        HttpRequest req = new HttpRequest();                            
        req.setMethod('POST');
        req.setEndpoint(tokenEndpoint);
        req.setHeader('Content-type', 'application/x-www-form-urlencoded');
        req.setBody(body);
        Http http = new Http();               
        try{
            HTTPResponse res = http.send(req);
            if ( res.getStatusCode() == 200 ) {
                Map<String, Object> responseMap = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
                access_token = (String)responseMap.get('access_token');
            }else{
                System.debug('JWTBearerFlow Error Occurred '+res.getBody());
            }
        }catch(Exception ex){
            if(String.valueOf(ex).startsWith('Unauthorized endpoint')){
                System.debug('JWTBearerFlow Please check Setup->Security->Remote site settings and add '+tokenEndpoint);
            }else{
                System.debug('JWTBearerFlow  '+ex.getStackTraceString());
                System.debug('JWTBearerFlow '+ex);
            }
        }
        return access_token;
    }
}

EinsteinOCRService Class which is used for reading the API Key and getting the access token.

public class EinsteinOCRService {
    
    public static FINAL STRING END_POINT = 'https://api.einstein.ai/v2/oauth2/token';
    public static String getAccessToken() {
        
        ContentVersion base64Content = [SELECT Title, VersionData 
                                        FROM ContentVersion
                                        WHERE Title='einstein_platform' OR 
                                        Title='predictive_services'
                                        ORDER BY Title 
                                        LIMIT 1];
        String keyContents = base64Content.VersionData.tostring();
        keyContents = keyContents.replace('-----BEGIN RSA PRIVATE KEY-----', '');
        keyContents = keyContents.replace('-----END RSA PRIVATE KEY-----', '');
        keyContents = keyContents.replace('\n', '');
        
        // Get a new token
        JWT jwt = new JWT('RS256');
        // jwt.cert = 'JWTCert'; 
        // Uncomment this if you used a Salesforce certificate to sign up for an Einstein Platform account
        jwt.pkcs8 = keyContents; // Comment this if you are using jwt.cert
        jwt.iss = 'developer.force.com';
        jwt.sub = 'youremail@gmail.com';
        jwt.aud =  END_POINT;
        jwt.exp = '3600';
        String access_token = JWTBearerFlow.getAccessToken(END_POINT, jwt);
        return access_token;    
    }
}

References

 

Amit Singh
Amit Singhhttps://www.pantherschools.com/
Amit Singh aka @sfdcpanther/pantherschools, a Salesforce Technical Architect, Consultant with over 8+ years of experience in Salesforce technology. 21x Certified. Blogger, Speaker, and Instructor. DevSecOps Champion
Share this

Leave a review

Excellent

SUBSCRIBE-US

Book a 1:1 Call

Must-read

How to start your AI Journey?

Table of Contents Introduction Are you tired of the same old world? Do you dream of painting landscapes with your code, composing symphonies with data, or...

The Secret Weapon: Prompt Engineering: 🪄

Table of Contents Introduction Crafting the perfect prompt is like whispering secret instructions to your AI muse. But there's no one-size-fits-all approach! Exploring different types of...

How to Singup for your own Production org?

Let's see how we can have our salesforce enterprise Salesforce Org for a 30-day Trial and use it as a Production environment. With the...

Recent articles

More like this

11 COMMENTS

  1. Hi thanks for the example. If I use token generated from the website, I can get the sentiment just fine.

    When I tried to get the assertions in Apex, why do I get Invalid JWT assertion?

    {
    “iss”: “developer.force.com”,
    “sub”: “my einstein api account”,
    “aud”: “https://api.einstein.ai/v2/oauth2/token”,
    “iat”: 1598227988,
    “exp”: 1598231588
    }

    • Have you followed the above steps ?
      Also,
      Sub – should be the email you signed up for platform api
      iat & exp are the time stamp should be generate dynamically and should be withing in next 5 min

  2. How to use Public key and Private to key to generate the access token, I am trying to authenticate Docusign using JWT.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

5/5

Stuck in coding limbo?

Our courses unlock your tech potential