Skip to content

Commit

Permalink
Digest authentication refactorized.
Browse files Browse the repository at this point in the history
  • Loading branch information
ibc committed Feb 28, 2013
1 parent 6867f51 commit 87357de
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 321 deletions.
193 changes: 115 additions & 78 deletions src/DigestAuthentication.js
Original file line number Diff line number Diff line change
@@ -1,131 +1,168 @@

/**
* @fileoverview SIP Digest Authentication
* @fileoverview DigestAuthentication
*/

/**
* SIP Digest Authentication.
* @augments JsSIP.
* @function Digest Authentication
* @param {JsSIP.UA} ua
* @param {JsSIP.OutgoingRequest} request
* @param {JsSIP.IncomingResponse} response
*/
(function(JsSIP) {
var DigestAuthentication,
LOG_PREFIX = JsSIP.name() +' | '+ 'DIGEST AUTHENTICATION' +' | ';

DigestAuthentication = function (ua, request, response) {
var authenticate, realm, qop, nonce, opaque,
username = ua.configuration.authorization_user,
password = ua.configuration.password;
DigestAuthentication = function(ua) {
this.username = ua.configuration.authorization_user;
this.password = ua.configuration.password;
this.cnonce = null;
this.nc = 0;
this.ncHex = '00000000';
this.response = null;
};


if(response.status_code === 401) {
authenticate = response.parseHeader('www-authenticate');
/**
* Performs Digest authentication given a SIP request and the challenge
* received in a response to that request.
* Returns true if credentials were successfully generated, false otherwise.
*
* @param {JsSIP.OutgoingRequest} request
* @param {JsSIP.IncomingResponse} response
*/
DigestAuthentication.prototype.authenticate = function(request, challenge) {
// Inspect and validate the challenge.

this.algorithm = challenge.algorithm;
this.realm = challenge.realm;
this.nonce = challenge.nonce;
this.opaque = challenge.opaque;
this.stale = challenge.stale;

if (this.algorithm) {
if (this.algorithm !== 'MD5') {
console.warn(LOG_PREFIX + 'challenge with Digest algorithm different than "MD5", authentication aborted');
return false;
}
} else {
authenticate = response.parseHeader('proxy-authenticate');
this.algorithm = 'MD5';
}

realm = authenticate.realm.replace(/"/g,'');
qop = authenticate.qop || null;
nonce = authenticate.nonce.replace(/"/g,'');
opaque = authenticate.opaque;

this.password = password;
this.method = request.method;
if (! this.realm) {
console.warn(LOG_PREFIX + 'challenge without Digest realm, authentication aborted');
return false;
}

this.username = username;
this.realm = realm;
this.nonce = nonce;
this.uri = request.ruri;
this.qop = qop;
this.response = null;
this.algorithm = "MD5";
this.opaque = opaque;
this.cnonce = null;
this.nc = 0;
};
if (! this.nonce) {
console.warn(LOG_PREFIX + 'challenge without Digest nonce, authentication aborted');
return false;
}

DigestAuthentication.prototype.authenticate = function(password) {
var ha1, ha2;
// 'qop' can contain a list of values (Array). Let's choose just one.
if (challenge.qop) {
if (challenge.qop.indexOf('auth') > -1) {
this.qop = 'auth';
} else if (challenge.qop.indexOf('auth-int') > -1) {
this.qop = 'auth-int';
} else {
// Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here.
console.warn(LOG_PREFIX + 'challenge without Digest qop different than "auth" or "auth-int", authentication aborted');
return false;
}
} else {
this.qop = null;
}

password = password || this.password;
// Fill other attributes.

this.method = request.method;
this.uri = request.ruri;
this.cnonce = JsSIP.Utils.createRandomToken(12);
this.nc += 1;
this.updateNcHex();

// nc-value = 8LHEX. Max value = 'FFFFFFFF'
// nc-value = 8LHEX. Max value = 'FFFFFFFF'.
if (this.nc === 4294967296) {
console.log(LOG_PREFIX + 'maximum "nc" value has been reached, resetting "nc"');
this.nc = 1;
this.ncHex = '00000001';
}

// Calculate the Digest "response" value.
this.calculateResponse();

return true;
};


/**
* Generate 'response' value.
* @private
*/
DigestAuthentication.prototype.calculateResponse = function() {
var ha1, ha2;

// HA1 = MD5(A1) = MD5(username:realm:password)
ha1 = JsSIP.Utils.calculateMD5(this.username + ":" + this.realm + ":" + password);
ha1 = JsSIP.Utils.calculateMD5(this.username + ":" + this.realm + ":" + this.password);

if (this.qop === 'auth' || this.qop === null) {
if (this.qop === 'auth') {
// HA2 = MD5(A2) = MD5(method:digestURI)
ha2 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri);
// response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2)
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth:" + ha2);

} else if (this.qop === 'auth-int') {
// HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody))
ha2 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri + ":" + JsSIP.Utils.calculateMD5(this.body ? this.body : ""));
}

if(this.qop === 'auth' || this.qop === 'auth-int') {
// response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2)
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.decimalToHex(this.nc) + ":" + this.cnonce + ":" + this.qop + ":" + ha2);
} else {
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth-int:" + ha2);

} else if (this.qop === null) {
// HA2 = MD5(A2) = MD5(method:digestURI)
ha2 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri);
// response = MD5(HA1:nonce:HA2)
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + ha2);
}

return this.toString();
};


DigestAuthentication.prototype.update = function(response) {
var authenticate, nonce;
/**
* Return the Proxy-Authorization or WWW-Authorization header value.
*/
DigestAuthentication.prototype.toString = function() {
var auth_params = [];

if(response.status_code === 401) {
authenticate = response.parseHeader('www-authenticate');
} else {
authenticate = response.parseHeader('proxy-authenticate');
if (! this.response) {
throw new Error('response field does not exist, cannot generate Authorization header');
}

nonce = authenticate.nonce.replace(/"/g,'');

if(nonce !== this.nonce) {
this.nc = 0;
this.nonce = nonce;
auth_params.push('algorithm=' + this.algorithm);
auth_params.push('username="' + this.username + '"');
auth_params.push('realm="' + this.realm + '"');
auth_params.push('nonce="' + this.nonce + '"');
auth_params.push('uri="' + this.uri + '"');
auth_params.push('response="' + this.response + '"');
if (this.opaque) {
auth_params.push('opaque="' + this.opaque + '"');
}
if (this.qop) {
auth_params.push('qop=' + this.qop);
auth_params.push('cnonce="' + this.cnonce + '"');
auth_params.push('nc=' + this.ncHex);
}

this.realm = authenticate.realm.replace(/"/g,'');
this.qop = authenticate.qop || null;
this.opaque = authenticate.opaque;
};


DigestAuthentication.prototype.toString = function() {
var authorization = 'Digest ';

authorization += 'username="' + this.username + '",';
authorization += 'realm="' + this.realm + '",';
authorization += 'nonce="' + this.nonce + '",';
authorization += 'uri="' + this.uri + '",';
authorization += 'response="' + this.response + '",';
authorization += this.opaque ? 'opaque="' + this.opaque + '",': '';
authorization += this.qop ? 'qop=' + this.qop + ',' : '';
authorization += this.qop ? 'cnonce="' + this.cnonce + '",' : '';
authorization += this.qop ? 'nc=' + this.decimalToHex(this.nc) + ',': '';
authorization += 'algorithm=MD5';

return authorization;
return 'Digest ' + auth_params.join(', ');
};


DigestAuthentication.prototype.decimalToHex = function(decimal) {
var hex = Number(decimal).toString(16);
return '00000000'.substr(0, 8-hex.length) + hex;
/**
* Generate the 'nc' value as required by Digest in this.ncHex by reading this.nc.
* @private
*/
DigestAuthentication.prototype.updateNcHex = function() {
var hex = Number(this.nc).toString(16);
this.ncHex = '00000000'.substr(0, 8-hex.length) + hex;
};

JsSIP.DigestAuthentication = DigestAuthentication;
}(JsSIP));
}(JsSIP));
Loading

1 comment on commit 87357de

@saghul
Copy link
Contributor

@saghul saghul commented on 87357de Feb 28, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍰

Please sign in to comment.