My workflow is to perform digest auth for a target endpoint. I was going to use the java callout that has been mentioned before by @dchiesa1, but I wanted more control over the debugging. I attempted it using a javascript callout, but keep running into the issue where the httpClient from Apigee's Javascript Object Model isn't giving back a nonce from the server to use in creating the final Authorization header. I've been using the following as a guide: https://github.com/inorganik/digest-auth-request/blob/master/digestAuthRequest.js
I've been using the crypto module for the MD5 piece.
I'd be happy to send what I have to a staff member, if there is something obvious I might be missing.
Solved! Go to Solution.
Following up here:
Was able to solve Digest Authentication with a couple of JavaScript Callouts and a Service Callout.
The challenge was always how to get the full WWW-Authenticate header value with the nonce.
In the PreFlow step of the specified Target Endpoint I placed the following JavaScript Callout to generate a dynamic URL to pass to the Service Callout:
(Reference: https://www.googlecloudcommunity.com/gc/Apigee/How-can-I-dynamically-set-the-URL-for-a-ServiceCallou...)
var targetUrl = context.getVariable("target.url");
var proxyPathSuffix = context.getVariable("proxy.pathsuffix");
var newServiceCalloutUrl = targetUrl + proxyPathSuffix;
print(newServiceCalloutUrl);
context.setVariable("dynamic_url_path", newServiceCalloutUrl);
context.setVariable("servicecallout.auth-callout.target.url", newServiceCalloutUrl);
After that the following Service Callback config looked like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ServiceCallout async="false" continueOnError="true" enabled="true" name="auth-callout">
<DisplayName>auth-callout</DisplayName>
<Properties/>
<Request clearPayload="true" variable="calloutRequest">
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<Set>
<Verb>GET</Verb>
<Path>{dynamic_url_path}</Path>
</Set>
</Request>
<Response>calloutResponse</Response>
<HTTPTargetConnection>
<URL>http://dummy.this.will.get.set.dynamically</URL>
</HTTPTargetConnection>
</ServiceCallout>
The Service Callback will return the WWW-Authenticate header when it resolves to a 401.
Because the WWW-Authenticate header is multi-valued it won't return properly in the httpClient from the Apigee JavaScript Object Model for some reason.
I ended up referencing the following to come up with a solution: https://www.googlecloudcommunity.com/gc/Cloud-Product-Articles/How-to-handle-multi-value-headers-in-...
By using the following line in the JavaScript Callout used to generate the new Authorization header, I was able to take the returned WWW-Authenticate header and parse it correctly.
Here is the final JavaScript Callout (Configuration followed by JavaScript file):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="500" name="digest-auth-init">
<DisplayName>digest-auth-init</DisplayName>
<Properties>
<Property name="proxySuffix">proxy.pathsuffix</Property>
<Property name="userName">*********</Property>
<Property name="password">*********</Property>
<Property name="method">request.verb</Property>
</Properties>
<ResourceURL>jsc://digestAuthApigee.js</ResourceURL>
</Javascript>
/*
Get properties from Javascript Callout
*/
var proxySuffixProp = properties.proxySuffix;
var proxySuffixValue = context.getVariable(proxySuffixProp);
var usernameValue = properties.userName;
var passwordValue = properties.password;
var methodProp = properties.method;
var methodValue = context.getVariable(methodProp);
var _crypto = crypto;
var credentials = {};
var cryptoUsingMD5 = function (data) {
var _md5 = _crypto.getMD5();
_md5.update(data);
return _md5.digest();
};
var parseAuthenticationInfo = function (authData) {
var authenticationObj = {};
authData.split(", ").forEach(function (d) {
d = d.split("=");
authenticationObj[d[0]] = d[1].replace(/"/g, "");
});
console.log(JSON.stringify(authenticationObj));
return authenticationObj;
};
var generateCnonce = function () {
var characters = "abcdef0123456789";
var token = "";
for (var i = 0; i < 16; i++) {
var randNum = Math.round(Math.random() * characters.length);
token += characters.substring(randNum, 1);
}
return token;
};
var getSecondMatchValue = function(source, regex){
var matches = regex.exec(source);
if(matches && matches.length > 1){
return matches[1];
}
else{
print("No matches!");
return "";
}
}
//https://www.googlecloudcommunity.com/gc/Cloud-Product-Articles/How-to-handle-multi-value-headers-in-Javascript/ta-p/76176
var wwwAuthenticate = context.getVariable(
"calloutResponse.header.www-authenticate.values.string"
);
if (wwwAuthenticate) {
print(wwwAuthenticate);
print("Successfully Authenticated!");
//The regex/substring is because regex positive lookahead wasn"t working
var realmRegex = new RegExp(/realm="(.+?)"/g);
var realm = getSecondMatchValue(wwwAuthenticate, realmRegex);
print(realm);
var realm = realm.replace("realm=", "").replace('"', "");
var nonceRegex = new RegExp(/nonce="(.*?)"/g);
var nonce = getSecondMatchValue(wwwAuthenticate, nonceRegex);
print(nonce);
var nonce = nonce.replace("nonce=", "").replace('"', "");
var qopRegex = new RegExp(/qop="(.*?)"/g);
var qop = getSecondMatchValue(wwwAuthenticate, qopRegex);
print(qop);
var qop = qop.replace("qop=", "").replace('"', "");
var opaqueRegex = new RegExp(/opaque="(.*?)"/g);
var opaque = getSecondMatchValue(wwwAuthenticate, opaqueRegex);
print(opaque);
var opaque = opaque.replace("opaque=", "").replace('"', "");
var nc = ("00000000" + 1).slice(-8);
var cnonce = generateCnonce();
var digestAuthObject = {};
digestAuthObject.ha1 = cryptoUsingMD5(
usernameValue + ":" + realm + ":" + passwordValue
);
digestAuthObject.ha2 = cryptoUsingMD5(methodValue + ":" + proxySuffixValue);
var resp = cryptoUsingMD5(
[digestAuthObject.ha1, nonce, nc, cnonce, qop, digestAuthObject.ha2].join(
":"
)
);
var digestAuthHeader =
'Digest username="' +
usernameValue +
'", ' +
'realm="' +
realm +
'", ' +
'nonce="' +
nonce +
'", ' +
'uri="' +
proxySuffixValue +
'", ' +
'response="' +
resp +
'", ' +
'opaque="' +
opaque +
'", ' +
"qop=" +
qop +
", " +
"nc=" +
("00000000" + nc).slice(-8) +
", " +
'cnonce="' +
cnonce +
'"';
print(digestAuthHeader);
context.setVariable("request.header.authorization", digestAuthHeader);
print(context.getVariable("request.header"));
}
(Note: I have a lot of print() statements for debugging that could be removed and I'm not using a KVM yet, so the reference to the username and password in the config and the JavaScript will need to change for that)
This was a pain to figure out. Perhaps someone else has figured out something better, but this worked for me. I don't encounter Digest Authentication a lot, so this might be one of those "nice-to-haves" if you run into it.
User | Count |
---|---|
1 | |
1 | |
1 | |
1 | |
1 |