You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					168 lines
				
				4.7 KiB
			
		
		
			
		
	
	
					168 lines
				
				4.7 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | var caseless = require('caseless') | ||
|  | var uuid = require('uuid/v4') | ||
|  | var helpers = require('./helpers') | ||
|  | 
 | ||
|  | var md5 = helpers.md5 | ||
|  | var toBase64 = helpers.toBase64 | ||
|  | 
 | ||
|  | function Auth (request) { | ||
|  |   // define all public properties here
 | ||
|  |   this.request = request | ||
|  |   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')) { | ||
|  |     self.request.emit('error', new Error('auth() received invalid user or password')) | ||
|  |   } | ||
|  |   self.user = user | ||
|  |   self.pass = pass | ||
|  |   self.hasAuth = true | ||
|  |   var header = user + ':' + (pass || '') | ||
|  |   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.
 | ||
|  |   //   - 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 | ||
|  |   while (true) { | ||
|  |     var match = re.exec(authHeader) | ||
|  |     if (!match) { | ||
|  |       break | ||
|  |     } | ||
|  |     challenge[match[1]] = match[2] || match[3] | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * 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) | ||
|  |   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.onRequest = function (user, pass, sendImmediately, bearer) { | ||
|  |   var self = this | ||
|  |   var request = self.request | ||
|  | 
 | ||
|  |   var authHeader | ||
|  |   if (bearer === undefined && user === undefined) { | ||
|  |     self.request.emit('error', new Error('no auth mechanism defined')) | ||
|  |   } else if (bearer !== undefined) { | ||
|  |     authHeader = self.bearer(bearer, sendImmediately) | ||
|  |   } else { | ||
|  |     authHeader = self.basic(user, pass, sendImmediately) | ||
|  |   } | ||
|  |   if (authHeader) { | ||
|  |     request.setHeader('authorization', authHeader) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | Auth.prototype.onResponse = function (response) { | ||
|  |   var self = this | ||
|  |   var request = self.request | ||
|  | 
 | ||
|  |   if (!self.hasAuth || self.sentAuth) { return null } | ||
|  | 
 | ||
|  |   var c = caseless(response.headers) | ||
|  | 
 | ||
|  |   var authHeader = c.get('www-authenticate') | ||
|  |   var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase() | ||
|  |   request.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(request.method, request.path, authHeader) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | exports.Auth = Auth |