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;
}
}