[6a3a178] | 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
|
---|