Salesforce callout with Digest Auth

Friday, June 1, 2018

Some time ago I had to create an apex callout to a webserver using digest access authentication

Unfortunately Salesforce callouts don't support digest authentication out of the box like Basic Auth or OAuth2. So I had to create my own DigestAuthHelper with some example anomynous apex to a dummy webserver from httpbin.org.

I hope this helps someone else. Don't hesitate to leave a comment below :-).

/* Anonymous Apex */
String username = 'myUser';
String password = 'myPassword';
String endpoint = 'https://httpbin.org/digest-auth/auth/myUser/myPassword';
String digestUri = '/digest-auth/auth/myUser/myPassword';
String method = 'GET';
String body = null;

HTTPResponse res = new DigestAuthHelper().send(username, password, endpoint, digestUri, method, body);

System.debug('res body ' + res.getBody());
System.debug('res code ' + res.getStatusCode());

The helper class. 

/* Class DigestAuthHelper: */
public class DigestAuthHelper {
    private String nonce = '';
    private String opaque = '';
    private String realm = '';
    private String qop = '';
    private String algorithm = '';
    private String cnonce = '';
    
    public HTTPResponse send(String username, String password, String endpoint, String digestUri, String method, String body){
        
        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod(method);
        
        if(!'GET'.equalsIgnoreCase(method) && String.isNotBlank(body)){
        	req.setBody(body);    
        }
 
        Http http = new Http();
        HTTPResponse res = http.send(req); 
        
        processAuthHeader(res);
        
        String h1str = getH1(username, password);
        System.debug('HA1 ' + h1str);
        
        String h2str = getH2(method, digestUri, Blob.valueOf(req.getBody()));
        System.debug('HA2 ' + h2str);
        
        String response = getResponse(h1str, h2str);
        System.debug('encoded response ' + response);
        
        String authHeader = getAuthHeader(username, digestUri, response);
        System.debug('authHeader ' + authHeader);
        
        req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod(method);
        req.setHeader('Authorization', authHeader);
        
        http = new Http();
        res = http.send(req);
        
        return res;
    }
    
    private String getH1(String username, String password){
        Blob targetBlob = null;
        Blob h1 = null;
        if(String.isBlank(algorithm) || algorithm.equalsIgnoreCase('MD5')){
            // If the algorithm directive's value is "MD5" or unspecified, then HA1 is
            // HA1=MD5(username:realm:password) 
            system.debug(username + ':' + realm + ':' + password);
            targetBlob = Blob.valueOf(username + ':' + realm + ':' + password);
            h1 = Crypto.generateDigest('MD5', targetBlob);
        } else{
            // If the algorithm directive's value is "MD5-sess", then HA1 is
            // HA1=MD5(MD5(username:realm:password):nonce:cnonce) 
            system.debug('MD5(' + username + ':' + realm + ':' + password + '):' + nonce + ':' + cnonce);
            targetBlob = Blob.valueOf(username + ':' + realm + ':' + password);
            h1 = Crypto.generateDigest('MD5', targetBlob);
            String h1str = EncodingUtil.convertToHex(h1);
            targetBlob = Blob.valueOf(h1str + ':' + nonce + ':' + cnonce);
            h1 = Crypto.generateDigest('MD5', targetBlob);
        }
        
        return EncodingUtil.convertToHex(h1);
    }
    
    private String getH2(String method, String digestUri, Blob body){
        Blob targetBlob = null;
        Blob h2 = null;
        // If the qop directive's value is "auth" or is unspecified, then HA2 is
        // HA2=MD5(method:digestURI)
        if(String.isBlank(qop) || qop.equalsIgnoreCase('auth')){
            targetBlob = Blob.valueOf(method + ':' + digestUri);  
        }
        else{
        	// If the qop directive's value is "auth-int", then HA2 is
        	// HA2=MD5(method:digestURI:MD5(entityBody))  
        	
            h2 = Crypto.generateDigest('MD5', body);
        	targetBlob = Blob.valueOf(method + ':' + digestUri + ':' + EncodingUtil.convertToHex(h2));  
        }
        
        h2 = Crypto.generateDigest('MD5', targetBlob);
        return EncodingUtil.convertToHex(h2);
    }
    
    private String getResponse(String h1str, String h2str){
        String responseRaw = '';
        Blob targetBlob = null;
        Blob auth = null;
        // If the qop directive's value is "auth" or "auth-int", then compute the response as follows:
        // response=MD5(HA1:nonce:nonceCount:cnonce:qop:HA2)
        
        if('auth'.equalsIgnoreCase(qop) || 'auth-int'.equalsIgnoreCase(qop)){
            responseRaw = h1str + ':' + nonce + ':00000001:' + cnonce + ':' + qop + ':' + h2str;
            System.debug('responseRaw ' + responseRaw);
            targetBlob = Blob.valueOf(responseRaw);
            auth = Crypto.generateDigest('MD5', targetBlob);
            return EncodingUtil.convertToHex(auth);
        }
        
        // If the qop directive is unspecified, then compute the response as follows:
        // response=MD5(HA1:nonce:HA2) 
        responseRaw = h1str + ':' + nonce + ':' + h2str;
        System.debug('responseRaw ' + responseRaw);
        targetBlob = Blob.valueOf(responseRaw);
        auth = Crypto.generateDigest('MD5', targetBlob);
        return EncodingUtil.convertToHex(auth);       
    }
    
    private String getAuthHeader(String username, String digestUri, String response){
        return 'Digest ' 
            + 'username="'+ username +'"'
            + ', realm="' + realm + '"'
            + ', nonce="' + nonce + '"' 
            + ', uri="'+ digestUri +'"'
            + ', response="' + response + '"' 
            + ', opaque="' + opaque + '"'
            + ', qop=' + qop 
            + ', nc=00000001'
            + ', cnonce="' + cnonce + '"';
    }
    
    private void processAuthHeader(HttpResponse res){
        String wwwAuthHeader = getAuthHeader(res);
        
        System.debug(wwwAuthHeader);
        String[] splitResp = wwwAuthHeader.replace('"','').split(',');
        
        for (String s : splitResp)
        {
            if (s.contains('nonce='))
                nonce = s.split('=')[1].trim();
            else if (s.contains('opaque='))
                opaque = s.split('=')[1].trim();
            else if (s.contains('realm='))
                realm = s.split('=')[1].trim();
            else if (s.contains('qop='))
                qop = s.split('=')[1].trim();
            else if (s.contains('algorithm='))
                algorithm = s.split('=')[1].trim();
        }   
        System.debug('nonce:' + nonce);  
        System.debug('opaque:' + opaque); 
        System.debug('realm:' + realm); 
        System.debug('qop:' + qop);
        System.debug('algorithm:' + algorithm);
        
        // generate random client nonce string
        String dateStr = String.valueOf(DateTime.now());
        cnonce = EncodingUtil.convertToHex(Blob.valueOf(dateStr));
        System.debug('cnonce ' + cnonce);
    }
    
    private static String getAuthHeader(HttpResponse res){
        String[] headers = res.getHeaderKeys();
        for(String header : headers){
            if(header.ContainsIgnoreCase('WWW-Authenticate'))
            {
                // getHeader is case sensitive.
                return res.getHeader(header);
            }
        }
        return null;
    }
}