auth.js 4.7KB

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