Get hands-on experience with 20+ free Google Cloud products and $300 in free credit for new customers.

Digest Authentication with Javascript and httpClient

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 Solved
1 2 3,319
1 ACCEPTED 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.

View solution in original post

2 REPLIES 2