auth.js (4778B)
1 'use strict' 2 3 var caseless = require('caseless') 4 var uuid = require('uuid/v4') 5 var helpers = require('./helpers') 6 7 var md5 = helpers.md5 8 var toBase64 = helpers.toBase64 9 10 function Auth (request) { 11 // define all public properties here 12 this.request = request 13 this.hasAuth = false 14 this.sentAuth = false 15 this.bearerToken = null 16 this.user = null 17 this.pass = null 18 } 19 20 Auth.prototype.basic = function (user, pass, sendImmediately) { 21 var self = this 22 if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) { 23 self.request.emit('error', new Error('auth() received invalid user or password')) 24 } 25 self.user = user 26 self.pass = pass 27 self.hasAuth = true 28 var header = user + ':' + (pass || '') 29 if (sendImmediately || typeof sendImmediately === 'undefined') { 30 var authHeader = 'Basic ' + toBase64(header) 31 self.sentAuth = true 32 return authHeader 33 } 34 } 35 36 Auth.prototype.bearer = function (bearer, sendImmediately) { 37 var self = this 38 self.bearerToken = bearer 39 self.hasAuth = true 40 if (sendImmediately || typeof sendImmediately === 'undefined') { 41 if (typeof bearer === 'function') { 42 bearer = bearer() 43 } 44 var authHeader = 'Bearer ' + (bearer || '') 45 self.sentAuth = true 46 return authHeader 47 } 48 } 49 50 Auth.prototype.digest = function (method, path, authHeader) { 51 // TODO: More complete implementation of RFC 2617. 52 // - handle challenge.domain 53 // - support qop="auth-int" only 54 // - handle Authentication-Info (not necessarily?) 55 // - check challenge.stale (not necessarily?) 56 // - increase nc (not necessarily?) 57 // For reference: 58 // http://tools.ietf.org/html/rfc2617#section-3 59 // https://github.com/bagder/curl/blob/master/lib/http_digest.c 60 61 var self = this 62 63 var challenge = {} 64 var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi 65 while (true) { 66 var match = re.exec(authHeader) 67 if (!match) { 68 break 69 } 70 challenge[match[1]] = match[2] || match[3] 71 } 72 73 /** 74 * RFC 2617: handle both MD5 and MD5-sess algorithms. 75 * 76 * If the algorithm directive's value is "MD5" or unspecified, then HA1 is 77 * HA1=MD5(username:realm:password) 78 * If the algorithm directive's value is "MD5-sess", then HA1 is 79 * HA1=MD5(MD5(username:realm:password):nonce:cnonce) 80 */ 81 var ha1Compute = function (algorithm, user, realm, pass, nonce, cnonce) { 82 var ha1 = md5(user + ':' + realm + ':' + pass) 83 if (algorithm && algorithm.toLowerCase() === 'md5-sess') { 84 return md5(ha1 + ':' + nonce + ':' + cnonce) 85 } else { 86 return ha1 87 } 88 } 89 90 var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth' 91 var nc = qop && '00000001' 92 var cnonce = qop && uuid().replace(/-/g, '') 93 var ha1 = ha1Compute(challenge.algorithm, self.user, challenge.realm, self.pass, challenge.nonce, cnonce) 94 var ha2 = md5(method + ':' + path) 95 var digestResponse = qop 96 ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) 97 : md5(ha1 + ':' + challenge.nonce + ':' + ha2) 98 var authValues = { 99 username: self.user, 100 realm: challenge.realm, 101 nonce: challenge.nonce, 102 uri: path, 103 qop: qop, 104 response: digestResponse, 105 nc: nc, 106 cnonce: cnonce, 107 algorithm: challenge.algorithm, 108 opaque: challenge.opaque 109 } 110 111 authHeader = [] 112 for (var k in authValues) { 113 if (authValues[k]) { 114 if (k === 'qop' || k === 'nc' || k === 'algorithm') { 115 authHeader.push(k + '=' + authValues[k]) 116 } else { 117 authHeader.push(k + '="' + authValues[k] + '"') 118 } 119 } 120 } 121 authHeader = 'Digest ' + authHeader.join(', ') 122 self.sentAuth = true 123 return authHeader 124 } 125 126 Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) { 127 var self = this 128 var request = self.request 129 130 var authHeader 131 if (bearer === undefined && user === undefined) { 132 self.request.emit('error', new Error('no auth mechanism defined')) 133 } else if (bearer !== undefined) { 134 authHeader = self.bearer(bearer, sendImmediately) 135 } else { 136 authHeader = self.basic(user, pass, sendImmediately) 137 } 138 if (authHeader) { 139 request.setHeader('authorization', authHeader) 140 } 141 } 142 143 Auth.prototype.onResponse = function (response) { 144 var self = this 145 var request = self.request 146 147 if (!self.hasAuth || self.sentAuth) { return null } 148 149 var c = caseless(response.headers) 150 151 var authHeader = c.get('www-authenticate') 152 var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase() 153 request.debug('reauth', authVerb) 154 155 switch (authVerb) { 156 case 'basic': 157 return self.basic(self.user, self.pass, true) 158 159 case 'bearer': 160 return self.bearer(self.bearerToken, true) 161 162 case 'digest': 163 return self.digest(request.method, request.path, authHeader) 164 } 165 } 166 167 exports.Auth = Auth