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

Refactor basic, bearer, digest auth logic into separate class #1360

Merged
merged 1 commit into from Jan 21, 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
133 changes: 133 additions & 0 deletions lib/auth.js
@@ -0,0 +1,133 @@
'use strict'

var caseless = require('caseless')
, uuid = require('node-uuid')
, helpers = require('./helpers')

var md5 = helpers.md5
, toBase64 = helpers.toBase64


function Auth () {
// define all public properties here
this.hasAuth = false
this.sentAuth = false
this.bearerToken = null
this.user = null
this.pass = null
}

Auth.prototype.basic = function (user, pass, sendImmediately) {
var self = this
if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) {
throw new Error('auth() received invalid user or password')
}
self.user = user
self.pass = pass
self.hasAuth = true
var header = typeof pass !== 'undefined' ? user + ':' + pass : user
if (sendImmediately || typeof sendImmediately === 'undefined') {
var authHeader = 'Basic ' + toBase64(header)
self.sentAuth = true
return authHeader
}
}

Auth.prototype.bearer = function (bearer, sendImmediately) {
var self = this
self.bearerToken = bearer
self.hasAuth = true
if (sendImmediately || typeof sendImmediately === 'undefined') {
if (typeof bearer === 'function') {
bearer = bearer()
}
var authHeader = 'Bearer ' + bearer
self.sentAuth = true
return authHeader
}
}

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?)
// - check challenge.stale (not necessarily?)
// - increase nc (not necessarily?)
// For reference:
// http://tools.ietf.org/html/rfc2617#section-3
// https://github.com/bagder/curl/blob/master/lib/http_digest.c

var self = this

var challenge = {}
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
for (;;) {
var match = re.exec(authHeader)
if (!match) {
break
}
challenge[match[1]] = match[2] || match[3]
}

var ha1 = md5(self.user + ':' + challenge.realm + ':' + self.pass)
var ha2 = md5(method + ':' + path)
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
var nc = qop && '00000001'
var cnonce = qop && uuid().replace(/-/g, '')
var digestResponse = qop
? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2)
: md5(ha1 + ':' + challenge.nonce + ':' + ha2)
var authValues = {
username: self.user,
realm: challenge.realm,
nonce: challenge.nonce,
uri: path,
qop: qop,
response: digestResponse,
nc: nc,
cnonce: cnonce,
algorithm: challenge.algorithm,
opaque: challenge.opaque
}

authHeader = []
for (var k in authValues) {
if (authValues[k]) {
if (k === 'qop' || k === 'nc' || k === 'algorithm') {
authHeader.push(k + '=' + authValues[k])
} else {
authHeader.push(k + '="' + authValues[k] + '"')
}
}
}
authHeader = 'Digest ' + authHeader.join(', ')
self.sentAuth = true
return authHeader
}

Auth.prototype.response = function (method, path, headers) {
var self = this
if (!self.hasAuth || self.sentAuth) { return null }

var c = caseless(headers)

var authHeader = c.get('www-authenticate')
var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase()
// debug('reauth', authVerb)

switch (authVerb) {
case 'basic':
return self.basic(self.user, self.pass, true)

case 'bearer':
return self.bearer(self.bearerToken, true)

case 'digest':
return self.digest(method, path, authHeader)
}
}

exports.Auth = Auth
114 changes: 15 additions & 99 deletions request.js
Expand Up @@ -27,6 +27,7 @@ var http = require('http')
, CombinedStream = require('combined-stream')
, isstream = require('isstream')
, getProxyFromURI = require('./lib/getProxyFromURI')
, Auth = require('./lib/auth').Auth

var safeStringify = helpers.safeStringify
, md5 = helpers.md5
Expand Down Expand Up @@ -487,6 +488,8 @@ Request.prototype.init = function (options) {
}

// Auth must happen last in case signing is dependent on other headers
self._auth = new Auth()

if (options.oauth) {
self.oauth(options.oauth)
}
Expand Down Expand Up @@ -1028,26 +1031,11 @@ Request.prototype.onRequestResponse = function (response) {
break
}
}
} else if (response.statusCode === 401 && self._hasAuth && !self._sentAuth) {
var authHeader = response.caseless.get('www-authenticate')
var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase()
debug('reauth', authVerb)

switch (authVerb) {
case 'basic':
self.auth(self._user, self._pass, true)
redirectTo = self.uri
break

case 'bearer':
self.auth(null, null, true, self._bearer)
redirectTo = self.uri
break

case 'digest':
self._digest(authHeader)
redirectTo = self.uri
break
} else if (response.statusCode === 401) {
var authHeader = self._auth.response(self.method, self.uri.path, response.headers)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of passing the raw headers, you could pass response.caseless and save yourself a second conversion step from headers -> caseless inside of auth.

Copy link
Member Author

Choose a reason for hiding this comment

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

I intentionally made it that way, so that the module knows less about the consumer, and it can work with more generic input data parameters.

Also in caseless that's just an assignment, so I don't think it degrades performance or anything like that.

this.dict = dict || {}

if (authHeader) {
self.setHeader('authorization', authHeader)
redirectTo = self.uri
}
}

Expand Down Expand Up @@ -1457,88 +1445,16 @@ var getHeader = Request.prototype.getHeader

Request.prototype.auth = function (user, pass, sendImmediately, bearer) {
var self = this

var authHeader
if (bearer !== undefined) {
self._bearer = bearer
self._hasAuth = true
if (sendImmediately || typeof sendImmediately === 'undefined') {
if (typeof bearer === 'function') {
bearer = bearer()
}
self.setHeader('authorization', 'Bearer ' + bearer)
self._sentAuth = true
}
return self
}
if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) {
throw new Error('auth() received invalid user or password')
}
self._user = user
self._pass = pass
self._hasAuth = true
var header = typeof pass !== 'undefined' ? user + ':' + pass : user
if (sendImmediately || typeof sendImmediately === 'undefined') {
self.setHeader('authorization', 'Basic ' + toBase64(header))
self._sentAuth = true
authHeader = self._auth.bearer(bearer, sendImmediately)
} else {
authHeader = self._auth.basic(user, pass, sendImmediately)
}
return self
}
Request.prototype._digest = function (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?)
// - check challenge.stale (not necessarily?)
// - increase nc (not necessarily?)
// For reference:
// http://tools.ietf.org/html/rfc2617#section-3
// https://github.com/bagder/curl/blob/master/lib/http_digest.c

var self = this

var challenge = {}
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
for (;;) {
var match = re.exec(authHeader)
if (!match) {
break
}
challenge[match[1]] = match[2] || match[3]
}

var ha1 = md5(self._user + ':' + challenge.realm + ':' + self._pass)
var ha2 = md5(self.method + ':' + self.uri.path)
var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth'
var nc = qop && '00000001'
var cnonce = qop && uuid().replace(/-/g, '')
var digestResponse = qop ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) : md5(ha1 + ':' + challenge.nonce + ':' + ha2)
var authValues = {
username: self._user,
realm: challenge.realm,
nonce: challenge.nonce,
uri: self.uri.path,
qop: qop,
response: digestResponse,
nc: nc,
cnonce: cnonce,
algorithm: challenge.algorithm,
opaque: challenge.opaque
}

authHeader = []
for (var k in authValues) {
if (authValues[k]) {
if (k === 'qop' || k === 'nc' || k === 'algorithm') {
authHeader.push(k + '=' + authValues[k])
} else {
authHeader.push(k + '="' + authValues[k] + '"')
}
}
if (authHeader) {
self.setHeader('authorization', authHeader)
}
authHeader = 'Digest ' + authHeader.join(', ')
self.setHeader('authorization', authHeader)
self._sentAuth = true

return self
}
Expand Down