Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for RFC 2617 MD5-sess algorithm. #1821

Merged
merged 1 commit into from Oct 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 19 additions & 4 deletions lib/auth.js
Expand Up @@ -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?)
Expand All @@ -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)
Expand Down
99 changes: 84 additions & 15 deletions tests/test-digest-auth.js
Expand Up @@ -3,6 +3,7 @@
var http = require('http')
, request = require('../index')
, tape = require('tape')
, crypto = require('crypto')

function makeHeader() {
return [].join.call(arguments, ', ')
Expand All @@ -12,23 +13,27 @@ 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

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
Expand All @@ -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') {
Expand Down Expand Up @@ -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

Expand Down