From 033e7fc42a0f40adf5e83644dd329625f32a2192 Mon Sep 17 00:00:00 2001 From: Dennis Keller Date: Mon, 5 Oct 2015 23:37:44 -0400 Subject: [PATCH] Implement support for 2617 MD5-sess algorithm. Update TODO list Remove challenge.algorithm TODO note --- lib/auth.js | 23 +++++++-- tests/test-digest-auth.js | 99 +++++++++++++++++++++++++++++++++------ 2 files changed, 103 insertions(+), 19 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index 1be1f4258..1cb695216 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -50,8 +50,6 @@ Auth.prototype.bearer = function (bearer, sendImmediately) { Auth.prototype.digest = function (method, path, authHeader) { // TODO: More complete implementation of RFC 2617. - // - check challenge.algorithm - // - support algorithm="MD5-sess" // - handle challenge.domain // - support qop="auth-int" only // - handle Authentication-Info (not necessarily?) @@ -73,11 +71,28 @@ Auth.prototype.digest = function (method, path, authHeader) { challenge[match[1]] = match[2] || match[3] } - var ha1 = md5(self.user + ':' + challenge.realm + ':' + self.pass) - var ha2 = md5(method + ':' + path) + /** + * RFC 2617: handle both MD5 and MD5-sess algorithms. + * + * If the algorithm directive's value is "MD5" or unspecified, then HA1 is + * HA1=MD5(username:realm:password) + * If the algorithm directive's value is "MD5-sess", then HA1 is + * HA1=MD5(MD5(username:realm:password):nonce:cnonce) + */ + var ha1Compute = function (algorithm, user, realm, pass, nonce, cnonce) { + var ha1 = md5(user + ':' + realm + ':' + pass) + if (algorithm && algorithm.toLowerCase() === 'md5-sess') { + return md5(ha1 + ':' + nonce + ':' + cnonce) + } else { + return ha1 + } + } + var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth' var nc = qop && '00000001' var cnonce = qop && uuid().replace(/-/g, '') + var ha1 = ha1Compute(challenge.algorithm, self.user, challenge.realm, self.pass, challenge.nonce, cnonce) + var ha2 = md5(method + ':' + path) var digestResponse = qop ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) : md5(ha1 + ':' + challenge.nonce + ':' + ha2) diff --git a/tests/test-digest-auth.js b/tests/test-digest-auth.js index 5ea141ee5..c05fea97d 100644 --- a/tests/test-digest-auth.js +++ b/tests/test-digest-auth.js @@ -3,6 +3,7 @@ var http = require('http') , request = require('../index') , tape = require('tape') + , crypto = require('crypto') function makeHeader() { return [].join.call(arguments, ', ') @@ -12,6 +13,10 @@ function makeHeaderRegex() { return new RegExp('^' + makeHeader.apply(null, arguments) + '$') } +function md5 (str) { + return crypto.createHash('md5').update(str).digest('hex') +} + var digestServer = http.createServer(function(req, res) { var ok , testHeader @@ -19,16 +24,16 @@ var digestServer = http.createServer(function(req, res) { if (req.url === '/test/') { if (req.headers.authorization) { testHeader = makeHeaderRegex( - 'Digest username="test"', - 'realm="Private"', - 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"', - 'uri="/test/"', - 'qop=auth', - 'response="[a-f0-9]{32}"', - 'nc=00000001', - 'cnonce="[a-f0-9]{32}"', - 'algorithm=MD5', - 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' + 'Digest username="test"', + 'realm="Private"', + 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"', + 'uri="/test/"', + 'qop=auth', + 'response="[a-f0-9]{32}"', + 'nc=00000001', + 'cnonce="[a-f0-9]{32}"', + 'algorithm=MD5', + 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' ) if (testHeader.test(req.headers.authorization)) { ok = true @@ -40,11 +45,53 @@ var digestServer = http.createServer(function(req, res) { // No auth header, send back WWW-Authenticate header ok = false res.setHeader('www-authenticate', makeHeader( - 'Digest realm="Private"', - 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"', - 'algorithm=MD5', - 'qop="auth"', - 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' + 'Digest realm="Private"', + 'nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93"', + 'algorithm=MD5', + 'qop="auth"', + 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' + )) + } + } else if (req.url === '/test/md5-sess') { // RFC 2716 MD5-sess w/ qop=auth + var user = 'test' + var realm = 'Private' + var pass = 'testing' + var nonce = 'WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93' + var nonceCount = '00000001' + var qop = 'auth' + var algorithm = 'MD5-sess' + if (req.headers.authorization) { + + //HA1=MD5(MD5(username:realm:password):nonce:cnonce) + //HA2=MD5(method:digestURI) + //response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2) + + var cnonce = /cnonce="(.*)"/.exec(req.headers.authorization)[1] + var ha1 = md5(md5(user + ':' + realm + ':' + pass) + ':' + nonce + ':' + cnonce) + var ha2 = md5('GET:/test/md5-sess') + var response = md5(ha1 + ':' + nonce + ':' + nonceCount + ':' + cnonce + ':' + qop + ':' + ha2) + + testHeader = makeHeaderRegex( + 'Digest username="' + user + '"', + 'realm="' + realm + '"', + 'nonce="' + nonce + '"', + 'uri="/test/md5-sess"', + 'qop=' + qop, + 'response="' + response + '"', + 'nc=' + nonceCount, + 'cnonce="' + cnonce + '"', + 'algorithm=' + algorithm + ) + + ok = testHeader.test(req.headers.authorization) + } else { + // No auth header, send back WWW-Authenticate header + ok = false + res.setHeader('www-authenticate', makeHeader( + 'Digest realm="' + realm + '"', + 'nonce="' + nonce + '"', + 'algorithm=' + algorithm, + 'qop="' + qop + '"' )) } } else if (req.url === '/dir/index.html') { @@ -112,6 +159,28 @@ tape('with sendImmediately = false', function(t) { }) }) +tape('with MD5-sess algorithm', function(t) { + var numRedirects = 0 + + request({ + method: 'GET', + uri: 'http://localhost:6767/test/md5-sess', + auth: { + user: 'test', + pass: 'testing', + sendImmediately: false + } + }, function(error, response, body) { + t.equal(error, null) + t.equal(response.statusCode, 200) + t.equal(numRedirects, 1) + t.end() + }).on('redirect', function() { + t.equal(this.response.statusCode, 401) + numRedirects++ + }) +}) + tape('without sendImmediately = false', function(t) { var numRedirects = 0