• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3var caseless = require('caseless')
4var uuid = require('uuid/v4')
5var helpers = require('./helpers')
6
7var md5 = helpers.md5
8var toBase64 = helpers.toBase64
9
10function 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
20Auth.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
36Auth.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
50Auth.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  for (;;) {
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
126Auth.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
143Auth.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
167exports.Auth = Auth
168