Web Server Flow using PKCE (Salesforce/SFDC)

Problem Statement: We get authorization code in the callback url and hacker can intercept the auth code and this way, without user’s knowledge, he can get access token and then salesforce resources if he has access to connect app’s client and client secret.

Solution: Salesforce recommends web server flow with Proof Key for Code for Exchange (PKCE, pronounced “pixy”) to help prevent authorization code interception attacks.

We will use the code_challenge and code_verifier parameters to implement PKCE with the web server flow. code_challenge is passed in Auth request and code_verifier is passed in token request. If these values are mismatched, invalid_grant error code is thrown.

We will generate code_verifier from 5 digit number only as shown below –

code_verifier = String.valueOf(Integer.valueof((Math.random() * 1000)));

code_challenge is SHA256 hash value of the code_verifier which we will generate using below method-

code_challenge = WebServerFlowController.getCodeChallenge(code_verifier);
public static String getCodeChallenge(String codeVerifier) {
	return base64URLencode(Crypto.generateDigest('SHA-256', Blob.valueOf(codeVerifier)));
}
    
private static 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;
}

Now lets see the complete apex code where I will passing these parameters as attributes in auth and token request-

public class WebServerFlowController  {

    //Use your Client Id
    String clientId = '****';

    //Use your Client Secret
    String clientsecret='*****';

    String accesstoken_url='https://login.salesforce.com/services/oauth2/token';

    String authurl='https://login.salesforce.com/services/oauth2/authorize';
    
    String creationEndPoint ='/services/data/v49.0/sobjects/Contact';

    String redirect_uri = 'https://sandeepcompany2-dev-ed--c.visualforce.com/apex/WebServerFlowPage';
    
    String access_token;
    String instance_url;
    static String code_verifier {get;set;}
    static String code_challenge {get;set;}
    
    public PageReference getAuthCode() {
        code_verifier = String.valueOf(Integer.valueof((Math.random() * 1000))); //generates a random number from 0 to 1000
        code_challenge = WebServerFlowController.getCodeChallenge(code_verifier);
    	List<Integration_Configuraiton__c> integrationConfigList = [Select Id, Code_Verifier__c, code_challenge__c from Integration_Configuraiton__c];
        if(integrationConfigList.size() > 0) {
            integrationConfigList[0].Code_Verifier__c = code_verifier;
            integrationConfigList[0].code_challenge__c = code_challenge;
        } else {
            integrationConfigList.add(new Integration_Configuraiton__c(Code_Verifier__c = code_verifier));
        }
		upsert integrationConfigList;        
            
        PageReference pageRef = new PageReference(authurl + '?response_type=code&client_id='+clientId+
                                                  '&redirect_uri='+redirect_uri+'&code_challenge='+code_challenge); //+ '&code_challenge_method=S256'
        pageRef.setRedirect(true);
        return pageRef;
    }
    
    public WebServerFlowController () {
        
    }
    
    public PageReference getAccessToken() {
        Integration_Configuraiton__c integrationConfig = [Select Id, Code_Verifier__c, code_challenge__c from Integration_Configuraiton__c];
        code_verifier = integrationConfig.Code_Verifier__c;
        code_challenge = integrationConfig.code_challenge__c;
        
        Http h= new Http();
        String code = ApexPages.currentPage().getParameters().get('code');        
        HttpRequest req= new HttpRequest();
        String reqbody = 'code=' + code + '&grant_type=authorization_code&client_id='+clientId+
            '&redirect_uri='+redirect_uri+'&client_secret='+clientsecret+'&code_verifier='+code_verifier;
        
        req.setBody(reqbody);
        
        req.setMethod('POST');
        
        req.setEndpoint(accesstoken_url);
        
        HttpResponse res=h.send(req);
        
        AccessTokenWrapper obj = AccessTokenWrapper.parse(res.getBody());
        access_token = obj.access_token;
        instance_url = obj.instance_url;
        apexpages.addMessage(new ApexPages.message(Apexpages.Severity.INFO, code_verifier + '***' + code_challenge + '####' + reqbody + '$$$$' + res.getBody()));
        return null;
    }
    
    public PageReference createContact() {
        Http h2 = new Http();
        String jsonstr = '{"FirstName": "Sandeep", "LastName" : "Kumar ' + System.now() + '"}';
    
        HttpRequest req1 = new HttpRequest();
    
        req1.setHeader('Authorization','Bearer ' + access_token);
    
        req1.setHeader('Content-Type','application/json');
    
        req1.setHeader('accept','application/json');
    
        req1.setBody(jsonstr);
    
        req1.setMethod('POST');
    
        req1.setEndpoint(instance_url + creationEndPoint);
    
        HttpResponse res1 = h2.send(req1);
        apexpages.addMessage(new ApexPages.message(Apexpages.Severity.INFO, res1.getBody()));
        return null;
    }
    
    public static String getCodeChallenge(String codeVerifier) {
        return base64URLencode(Crypto.generateDigest('SHA-256', Blob.valueOf(codeVerifier)));
	}
    
    private static 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;
    }
}

In above code, I am using custom setting for temporary storage of code verifier and code challenge. Now Its your choice, if you want to keep these values on some custom object.

To see AccessTokenWrapper class and visualforce for WebServerFlowController, please visit my previous blog OAuth 2.0 Web Server Flow for Web App Integration (SFDC, Salesforce) where I am explained the basics of web server flow and the complete steps.

Demo (Detailed Explanation):

Published by Sandeep Kumar

He is a Salesforce Certified Application Architect having 11+ years of experience in Salesforce.

Leave a Reply