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 1,829
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

Perhaps this will help get the conversation going.  Obviously, it's not a working example, just what I've cribbed together.
In the XML configuration for the JavaScript Callout are the properties and clear text strings being used in the callout.

If anyone has an idea on why the httpClient is not returning a nonce, that would be helpful.  Perhaps the implementation of the httpClient itself is missing something?

My very elementary understanding on Digest Authentication is that there needs to be no Authorization header in the initial request.

Thank you in advance for any feed back.  I am not looking for a solution where JavaScript is not involved.  If there is a way to create an issue/bug report for the httpClient, perhaps that would be a better place to start.  I don't know where that form would be.

/*
    Get properties from Javascript Callout
*/
var callingUrlValue = properties.targetUrl;

var proxySuffixProp = properties.proxySuffix;
var proxySuffixValue = context.getVariable(proxySuffixProp);
callingUrlValue += proxySuffixValue;

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 onComplete = function (response, error) {
  if (error) {
    print(error);
  }

  if (response) {
    print(JSON.stringify(response.headers));
    print("Successfully Authenticated!");

    var wwwAuthHeader = response.headers["WWW-Authenticate"];

    print(wwwAuthHeader);

    //The regex/substring is because regex positive lookahead wasn"t working
    var realmRegex = new RegExp(/realm="(.+?)"/g);
    var realm = wwwAuthHeader.match(realmRegex)[1];
    print(realm);
    var realm = realm.replace("realm=", "").replace("\"", "");

    var nonceRegex = new RegExp(/nonce="(.*?)"/g);
    var nonce = wwwAuthHeader.match(nonceRegex)[1];
    print(nonce);
    var nonce = nonce.replace("nonce=", "").replace("\"", "");

    var qopRegex = new RegExp(/qop="(.*?)"/g);
    var qop = wwwAuthHeader.match(qopRegex)[1];
    print(qop);
    var qop = qop.replace("qop=", "").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=\"" + 
      username +
      "\", " + 
      "realm=\"" + 
      realm +
      "\", " +
      "nonce=\"" +  
      nonce +
      "\", " +
      "uri=\"" +
      url +
      "\", " +
      "response=\"" +
      resp +
      "\", " +
      "opaque=\"" +
      self.opaque +
      "\", " +
      "qop=" +
      qop +
      ", " +
      "nc=" +
      ("00000000" + self.nc).slice(-8) +
      ", " +
      "cnonce=\"" +
      self.cnonce +
      "\"";

    print(digestAuthHeader);

    context.setVariable("request.header.authorization", digestAuthHeader);

    print(context.getVariable("request.header"));
  }
};

var request = new Request(callingUrlValue, "get", {});
httpClient.send(request, onComplete);

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.