Tuesday, August 18, 2020

Custom Client Credential Grant Type Auth Provider in Salesforce for Named Credentials based Webservice Invocation

 If we have to create named credentials for a OAuth 2.0(client credentials grant type) based webservice endpoint in Salesforce we have to provide authentication provider. All of the standard auth provider type supported by salesforce are authorization code based grant types. In order to overcome this we will have to go for a custom auth provider where we can override some of the authorization flow behavior to get the bearer token and refresh token.

Lets start with the implementation in following steps

Step 1 - Create a custom auth provider

We can create a custom auth provider by extending standard salesforce class Auth.AuthProviderPluginClass 
Please refer the below code for reference

/**
 Developer : Tarique Habibullah
 forked from https://github.com/bobbywhitesfdc/ApigeeAuthProvider
**/
public class MyCustomAuth extends Auth.AuthProviderPluginClass{

    public static final String RESOURCE_CALLBACK = '/services/authcallback/';
    public static final String DEFAULT_TOKEN_TYPE = 'BearerToken';
    public static final String ENCODING_XML = 'application/x-www-form-urlencoded;charset=UTF-8';
    public static final String ENCODING_JSON = 'application/json';
    public static final String DUMMY_CODE = '999';
    public static final String DOUBLEQUOTE = '"';

    // This class is dependant on this Custom Metadata Type created to hold custom parameters
    public static final String CUSTOM_MDT_NAME = 'MyCustomMetadata__mdt'; 
    public static final String CMT_FIELD_CALLBACK_URL = 'Callback_URL__c';
    public static final String CMT_FIELD_PROVIDER_NAME = 'Auth_Provider_Name__c';
    public static final String CMT_FIELD_AUTHTOKEN_URL = 'Access_Token_URL__c';
    public static final String CMT_FIELD_CLIENT_ID = 'Client_Id__c';
    public static final String CMT_FIELD_CLIENT_SECRET = 'Client_Secret__c';
    public static final String CMT_FIELD_USE_JSON = 'Use_JSON_Encoding__c';
    public static final String CMT_FIELD_SCOPE = 'Scope__c';

    public static final String GRANT_TYPE_PARAM = 'grant_type';
    public static final String CLIENT_ID_PARAM = 'client_id';
    public static final String CLIENT_SECRET_PARAM = 'client_secret';
    public static final String SCOPE_PARAM = 'scope';
    public static final String GRANT_TYPE_CLIENT_CREDS = 'client_credentials';


    /**
     Added Constructor purely for debugging purposes to have visibility as to when the class
     is being instantiated.
    **/
    public MyCustomAuth() {
        super();
        System.debug('Constructor called');
    }
    
    
    /**
        Name of custom metadata type to store this auth provider configuration fields
        This method is required by its abstract parent class.

    **/
    public String getCustomMetadataType() {
        return CUSTOM_MDT_NAME;
    } 
    
    /**
    Initiate callback. No End User authorization required in this flow so skip straight to the Token request.
    The interface requires the callback url to be defined. 
    Eg: https://test.salesforce.com/services/authcallback/
    **/
    public PageReference initiate(Map config, String stateToPropagate) {
        System.debug('initiate');

        final PageReference pageRef = new PageReference(getCallbackUrl(config)); //NOSONAR
        pageRef.getParameters().put('state',stateToPropagate);
        pageRef.getParameters().put('code',DUMMY_CODE); // Empirically found this is required, but unused
        System.debug(pageRef.getUrl());
        return pageRef;
    } 

    /**
      This method composes the callback URL automatically UNLESS it has been overridden through Configuration.
      Normally one should not override the callback URL, but it's there in case the generated URL doesn't work.
    **/
    private String getCallbackUrl(Map config) {
        // https://{salesforce-hostname}/services/authcallback/{urlsuffix}
        final String overrideUrl = config.get(CMT_FIELD_CALLBACK_URL);
        final String generatedUrl = URL.getSalesforceBaseUrl().toExternalForm() + RESOURCE_CALLBACK + config.get('Auth_Provider_Name__c');
        System.debug('>>>'+(String.isEmpty(overrideUrl) ? generatedUrl : overrideUrl));
        return String.isEmpty(overrideUrl) ? generatedUrl : overrideUrl;
        
    }
    
    /**
    Handle callback (from initial loop back "code" step in the flow).
    In the Client Credentials flow, this method retrieves the access token directly.

    Required by parent class.

    Error handling here is a bit painful as the UI never displays the exception or error message 
    supplied here.  The exception is thrown for Logging/Debugging purposes only. 
    **/
    public Auth.AuthProviderTokenResponse handleCallback(Map config, Auth.AuthProviderCallbackState state ) {
        System.debug('handleCallback');
        final TokenResponse response = retrieveToken(config);

        if (response.isError()) {
            throw new TokenException(response.getErrorMessage());
        }
        return new Auth.AuthProviderTokenResponse(config.get(CMT_FIELD_PROVIDER_NAME)
                                                  , response.access_token
                                                  , config.get(CMT_FIELD_CLIENT_SECRET) // No Refresh Token
                                                  , state.queryParameters.get('state'));
    } 
    
    /**
        Refresh is required by the parent class and it's used if the original Access Token has expired.
        In the Client Credentials flow, there is no Refresh token, so its implementation is exactly the
        same as the Initiate() step.
    **/
    public override Auth.OAuthRefreshResult refresh(Map config, String refreshToken) {
        System.debug('refresh');
        final TokenResponse response = retrieveToken(config);
        return new Auth.OAuthRefreshResult(response.access_token, response.token_type);
    }

       
    /**
        getUserInfo is required by the Parent class, but not fully supported by this provider.
        Effectively the Client Credentials flow is only useful for Server-to-Server API integrations
        and cannot be used for other contexts such as a Registration Handler for Communities.
     **/
    public Auth.UserData getUserInfo(Map config, Auth.AuthProviderTokenResponse response) {
        System.debug('getUserInfo-was-called');
        final TokenResponse token = retrieveToken(config);

        final Auth.UserData userData = new Auth.UserData(
              token.application_name // identifier
            , null // firstName
            , null // lastName
            , null // fullName
            , token.developer_email // email
            , null // link
            , token.developer_email // userName
            , null  //locale
            , config.get(CMT_FIELD_PROVIDER_NAME) //provider
            , null // siteLoginUrl
            , new Map());


        return userData;
    }
    
    
    /**
       Private method that gets the Auth Token using the Client Credentials Flow.
    **/
     private TokenResponse retrieveToken(Map config) {
         
        System.debug('retrieveToken');

        final Boolean useJSONEncoding = Boolean.valueOf(config.get(CMT_FIELD_USE_JSON));
        
        final HttpRequest req = new HttpRequest();

        final PageReference endpoint = new PageReference(config.get(CMT_FIELD_AUTHTOKEN_URL)); //NOSONAR -- Protected by RemoteSite Setting
        if (!useJSONEncoding) { // Including the Query String breaks JSON encoded OAuth
            endpoint.getParameters().put('grant_type',GRANT_TYPE_CLIENT_CREDS);            
        }

        // Determine whether or not to use JSON encoding
        final String encoding = useJSONEncoding ? ENCODING_JSON : ENCODING_XML;
        final String encodedParams = encodeParameters(config,encoding);

        System.debug('Endpoint: ' + endpoint.getUrl());
        System.debug('Content-Type:' + encoding);
        //System.debug('Body:' + encodedParams);

        req.setEndpoint(endpoint.getUrl()); 
        req.setHeader('Content-Type',encoding); 
        req.setMethod('POST'); 
        req.setBody(encodedParams);

        final HTTPResponse res = new Http().send(req); 

        System.debug('Token Response Status: ' + res.getStatus() + ' ' + res.getStatusCode());
        final Integer statusCode = res.getStatusCode();

        if ( statusCode == 200) {
            TokenResponse token =  deserializeToken(res.getBody());
            // Ensure values for key fields
            token.token_type = (token.token_type == null) ? DEFAULT_TOKEN_TYPE : token.token_type;
            return token;

        } else  {
            return deserializeToken(res.getBody());
        }

    }
    
    //deserialise response and return token
    @testVisible
    private TokenResponse deserializeToken(String responseBody) {
        
        System.debug('token response:' +responseBody);
        
        // use default parsing for everything we can.
        TokenResponse parsedResponse = (TokenResponse) System.JSON.deserialize(responseBody, TokenResponse.class);
        // explicitly parse out the developer.email property because it's an illegal identifier
        Map props = (Map) System.JSON.deserializeUntyped(responseBody);
        parsedResponse.developer_email = (String) props.get('developer.email');
       
        return parsedResponse;
    }

    /**
        Conditionally encode parameters as URL-style or JSON
    **/
    @testVisible
    private String encodeParameters(Map config,String encoding) {

        // Pull out the subset of configured parameters that will be sent
        Map params = new Map();
        params.put(GRANT_TYPE_PARAM,GRANT_TYPE_CLIENT_CREDS);
        params.put(CLIENT_ID_PARAM, config.get(CMT_FIELD_CLIENT_ID));
        params.put(CLIENT_SECRET_PARAM, config.get(CMT_FIELD_CLIENT_SECRET));
        final String scope = config.get(CMT_FIELD_SCOPE);
        if (!String.isEmpty(scope)) {
            params.put(SCOPE_PARAM,scope);
        }

        return encoding == ENCODING_JSON ? encodeAsJSON(params) : encodeAsURL(params);
    }

    private String encodeAsJSON(Map params) {
        String output = '{';
        for (String key : params.keySet()) {
            output += (output == '{' ? '' : ', ');
            output += DOUBLEQUOTE + key + DOUBLEQUOTE + ':';
            output += DOUBLEQUOTE + params.get(key) + DOUBLEQUOTE;
        }
        output += '}';
        return output;
    }

    private String encodeAsURL(Map params) {
        String output = '';
        for (String key : params.keySet()) {
            output += (String.isEmpty(output) ? '' : '&');
            output += key + '=' + params.get(key);
        }
        return output;
    }

    /**

    OAuth Response is a JSON body like this on a Successful call

    {
      "refresh_token_expires_in" : "0",
      "api_product_list" : "[helloworld, HelloWorld_OAuth2-Product]",
      "api_product_list_json" : [ "helloworld", "HelloWorld_OAuth2-Product" ],
      "organization_name" : "bobbywhite-eval",
      "developer.email" : "developer@example.com",
      "token_type" : "BearerToken",
      "issued_at" : "1520478821362",
      "client_id" : "someKey",
      "access_token" : "kRxqmPr2b223uzTUGnndQhXWv8F4",
      "application_name" : "47bc6c8d-34f3-4141-b9e6-f1679a8240e7",
      "scope" : "",
      "expires_in" : "3599",
      "refresh_count" : "0",
      "status" : "approved"
    }

    On failure, the following structure from Apigee Edge (cloud hosted Gateway)

    { 
      "ErrorCode" : "invalid_client"
    , "Error" : "Client credentials are invalid"
    }

    The following response class is the Union of all responses
    **/

    public class TokenResponse {
        public String refresh_token_expires_in {get;set;}
        public String api_product_list {get;set;}
        public List api_product_list_json {get;set;}
        public String organization_name {get;set;}
        public String developer_email {get;set;}
        public String token_type {get;set;}
        public String issued_at {get;set;}
        public String client_id {get;set;}
        public String access_token {get;set;}
        public String application_name {get;set;}
        public String scope {get;set;}
        public String expires_in {get;set;}
        public String refresh_count {get;set;}
        public String status {get;set;}

        // Apigee Edge -- hosted version uses these fields for error handling
        public String ErrorCode {get; set;}
        public String Error {get; set;}

        // Apigee on premise version uses this Field for error handling
        public Fault fault {get; set;}

        public Boolean isError() {
            return Error != null || fault != null;
        }

        public String getErrorMessage() {
            if (Error != null) {
                return ErrorCode;
            }

            if (fault != null) {
                // Substitute the error code to compose
                return fault.faultString.replace('{0}',fault.detail.errorcode);
            }
            return null;
        }
    }

    public class Fault {
        public String faultstring {get;set;}
        public Detail detail {get;set;}
    }

    public class Detail {
        public String errorcode {get;set;}
    }

    /**
        Custom exception type so we can wrap and rethrow
    **/
    public class TokenException extends Exception {

    }
}


Step 2 - Create custom metadata which will be used by auth provider class in step 1



Step 3 - Create new auth provider by selecting the custom auth provider 


Step 4 - define named credentials to use the new auth provider.


Step 5 - execute the following code to invoke the named credentials

/**
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_Named_Credential/some_path');
req.setMethod('GET');
Http http = new Http();
HTTPResponse res = http.send(req);
System.debug(res.getBody());
 
 


Author: Tarique Habibullah

Sunday, November 18, 2018

Invoking Salesforce REST API using JWT

Invoking Salesforce REST API using JWT

Brief introduction to JWT

JSON Web Token(JWT) is a open standard way of API communication as JSON object which are digitally signed. JWT can be signed using a secret (HMAC algorithm) or a public/private key pair using RSA or similar algorithms.
JSON Web Tokens are created by three parts Header, Payload & Signature each separated by a dot. header.payload.signature 
The header contains of two different information in form of json object which is the type of token & the algorithm name being used in the communication. The json value is base64 url encoded before using.
{
"typ": "JWT"
"alg": "HS256",
}

The payload is also in json format and contains the claim information which is the user who is initiating the communication with additional details. The word claim indicates that the user is claiming that the information belongs to it. The json value is base64 url encoded before using. Following fields are standard fields which can be used as part of claim set by internet drafts (source - Wikipedia)
code name description
iss Issuer Identifies principal that issued the JWT.
sub Subject Identifies the subject of the JWT.
aud Audience Identifies the recipients that the JWT is intended for. Each principal intended to process the JWT must identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT must be rejected
exp Expiration time Identifies the expiration time on or after which the JWT must not be accepted for processing. The value should be in NumericDate[10] format.
nbf Not before Identifies the time on which the JWT will start to be accepted for processing.
iat Issued at Identifies the time at which the JWT was issued.
jti JWT ID Case sensitive unique identifier of the token even among different issuers.
The signature is created by taking base64 url encoded header and payload and then creating the hash with the secret using the algorithm specified in the header.  The hash value is again base64 url encoded to get the signature. The signature value is used to make sure the value is not changed along the way.
The three values are appended with a dot to get the final token which can be sent over http for any reliable communication. The receiving end will verify the authenticity of the sender by using the public key to sign the data & comparing it with the signature.  The data inside JWT is encoded and signed and not encrypted so we should not pass any sensitive information with it. The purpose of JWT is to confirm and trust that the sent data was created by a authentic source.

Benefits of using JWT

1. When compared to Simple Web Tokens(SWT) & Security Assertion Markup Language Tokens(SAML) JWT uses JSON which occupies less size than XML.
2. SWT can only be symmetrically signed by a shared secret using the HMAC algorithm whereas JWT & SAML tokens can use a public/private key pair in the form of a X.509 certificate for signing.
3. In context of salesforce rest apis we dont need to store connected app secret or user password on whose behalf we are invoking the api.

Using JWT with Salesforce REST API

1. Create/import a certificate in your salesforce org 

Go to your salesforce org which will act as client and navigate to setup and then certificate and key management. You can create a self signed or CA signed certificate depending upon the requirement. you can also import certificate from external sources.

2. Create a connected app in the salesforce org

Go to your salesforce org which will act as client and navigate to setup and then app manager. Create a new connected app as shown in image below. We need to enable oauth settings and check use digital signature. We can download the certificate created in step 1 above from the certificate details page and upload here. You can copy the consumer key of the connected app to be used later.


3. Generate OAuth Bearer token to invoke REST Api

public class Jwt {
    private String iss{get;set;}
    private String sub{get;set;}
    private String aud{get;set;}
    private String tokenEndpointBaseUrl{get;set;}
    private String minutes{get;set;}
    private String certName{get;set;}

    public Jwt(String iss, String sub, String aud, String minutes, String certName, String tokenEndpointBaseUrl) {
        this.iss=iss;
        this.sub=sub;
        this.aud=aud;
        this.minutes=minutes;
        this.certName=certName;
        this.tokenEndpointBaseUrl=tokenEndpointBaseUrl;
    }


    public String getJwtBearerToken()
{
        String jwtHeader = '{"typ":"JWT","alg":"RS256"}';
        Long exp = DateTime.now().addMinutes(Integer.valueOf(minutes)).getTime();
        String jwtClaims = '{"iss":"' + iss + '","sub":"' + sub + '","aud":"' + aud + '","exp":' + exp + '}';
      /* Salesforce Apex doesnt have base64 encoding for url so we use Base64  and convert '+' to '-' and '/' with '_' */
        String jwtRequest = System.encodingUtil.base64Encode(Blob.valueOf(jwtHeader)).replace('+', '-').replace('/', '_') +
                '.' + System.encodingUtil.base64Encode(Blob.valueOf(jwtClaims)).replace('+', '-').replace('/', '_');
/* We are using certificate name with the Crypto.signWithCertificate api. If needed we can use Crypto.sign api which supports using the private key directly*/
        String signature = System.encodingUtil.base64Encode(Crypto.signWithCertificate('RSA-SHA256', Blob.valueOf(jwtRequest), certName)).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(tokenEndpointBaseUrl+'/services/oauth2/token');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
        req.setBody(payload);
        res = httpObj.send(req);
        return res.getBody();
    }
}
public class RestCall{
public void invoke(){
        boolean isSandbox= isSandBoxEnv();
        String oauthBaseUrl=getOauthBaseUrl(isSandbox);
        String res = new Jwt('<consumer key>'
                '<user on whose behalf to invoke the api>',
                oauthBaseUrl,
                '3',
                '<Certificate Name defined in the org>',
                oauthBaseUrl
        ).getJwtBearerToken();
        JSONParser parser = JSON.createParser(res);
        String token='';
        while (parser.nextToken() != null) {
            if (parser.getCurrentToken() == JSONToken.FIELD_NAME
                    && parser.getText() == 'access_token') {
                parser.nextToken();
                System.debug('res='+ parser.getText().length());
                token=parser.getText();
            }
        }
        HttpRequest request = new HttpRequest();
        String url='<complete REST url >';
        request.setEndpoint(url);
        request.setTimeout(2 * 60 * 1000);
        request.setMethod('POST');
        request.setHeader('Authorization', 'Bearer '+token);
        request.setHeader('Content-Type','application/json');
        request.setBody(jsonBody);
        Http http = new Http();
        HttpResponse response = http.send(request);
  }      

public boolean isSandBoxEnv(){
        boolean isSandbox=false;
        List<organization> lstOrganization = [Select id,instanceName,isSandbox from Organization];
        if(lstOrganization.size()>0) {
            if(lstOrganization[0].isSandbox) {
                isSandbox=true;
            }
        }
        return isSandbox;
    }

    public String getOauthBaseUrl(boolean isSandbox){
        String url='https://login.salesforce.com';
        if(isSandbox){
            url='https://test.salesforce.com';
        }
        return url;
    }
}


4. Authorization by the user to the connected app

The user on behalf of whom we are going to invoke the API needs to authorize the connected app(one time)  using the following url in a browser for access by backend apex code. The user needs to login using the salesforce credentials.

https://login.salesforce.com/services/oauth2/authorize?
client_id=xxxxxxxxxx&
redirect_uri=https://login.salesforce.com/services/oauth2/success&
response_type=code
In case of use by front end javascript code, the authorization can be taken on the go using javascript & invoking the url in the same browser window. We need to use test.salesforce.com instead of login.salesforce.com for sandbox.

Author: Tarique Habibullah