http-auth
Version:
Node.js package for HTTP basic and digest access authentication.
122 lines (97 loc) • 4.02 kB
text/coffeescript
# Base authentication module.
Base = require './base'
# Utility module.
utils = require './utils'
# UUID module.
uuid = require 'node-uuid'
# Digest authentication class.
class Digest extends Base
# Constructor.
constructor: (@options, @checker) ->
super @options, @checker
# Array of random strings sent to clients.
@nonces = []
# Algorithm of encryption, could be MD5 or MD5-sess, default is MD5.
@options.algorithm = if @options.algorithm is 'MD5-sess' then 'MD5-sess' else 'MD5'
# Quality of protection is by default auth.
@options.qop = if @options.qop is 'none' then '' else 'auth'
# Processes line from authentication file.
processLine: (line) ->
[username, realm, hash] = line.split ":"
if realm is @options.realm # We need only users for given realm.
@options.users.push {username: username, hash: hash}
# Parse authorization header.
parseAuthorization: (header) ->
opts = {}
parts = header.split(' ')
params = parts.slice(1).join(' ')
tokens = params.split(/,(?=(?:[^"]|"[^"]*")*$)/) # Split the parameters by comma.
if (parts[0].substr 0, 6) is "Digest" # is Digest.
# Parse parameters.
i = 0
len = tokens.length
while i < len
param = /(\w+)=["]?([^"]*)["]?$/.exec(tokens[i]) # Strip quotes and whitespace.
if param
opts[param[1]] = param[2]
i++
opts
# Validating hash.
validate: (ha2, co, hash) ->
ha1 = hash
if co.algorithm is 'MD5-sess' # Algorithm.
ha1 = utils.md5 "#{ha1}:#{co.nonce}:#{co.cnonce}"
if co.qop # Quality of protection.
response = utils.md5 "#{ha1}:#{co.nonce}:#{co.nc}:#{co.cnonce}:#{co.qop}:#{ha2}"
else
response = utils.md5 "#{ha1}:#{co.nonce}:#{ha2}"
# Returning result.
response is co.response
# Searching for user.
findUser: (req, co, callback) ->
if @validateNonce co.nonce
ha2 = utils.md5 "#{req.method}:#{co.uri}"
if @checker # Custom authentication.
@checker.apply this, [co.username, (hash) =>
callback.apply this, [{user: co.username if (@validate ha2, co, hash)}]
]
else # File based.
for user in @options.users # Loop users to find the matching one.
if user.username is co.username and @validate ha2, co, user.hash
found = true
break # Stop searching, we found him.
callback.apply this, [{user: co.username if found}]
else
callback.apply this, [{stale: true}]
# Remove nonces.
removeNonces: (noncesToRemove) ->
for nonce in noncesToRemove
index = @nonces.indexOf nonce
if index != -1 # Nonce found.
@nonces.splice index, 1 # Remove it from array.
# Validate nonce.
validateNonce: (nonce) ->
now = Date.now() # Current time.
noncesToRemove = [] # Nonces for removal.
for serverNonce in @nonces # Searching for not expired ones.
if (serverNonce[1] + 3600000) > now # Not expired ones (1 hour lifetime).
if serverNonce[0] is nonce
found = true
else # Removing expired ones.
noncesToRemove.push serverNonce
@removeNonces noncesToRemove
return found
# Generates and returns new random nonce.
askNonce: () ->
nonce = utils.md5 uuid.v4() # Random nonce.
@nonces.push [nonce, Date.now()] # Push into nonces.
return nonce # Return it.
# Generates request header.
generateHeader: (result) ->
nonce = @askNonce()
stale = if result.stale then true else false
# Returning it.
return "Digest realm=\"#{@options.realm}\", qop=\"#{@options.qop}\", nonce=\"#{nonce}\", algorithm=\"#{@options.algorithm}\", stale=\"#{stale}\""
# Exporting.
module.exports = (options, checker) ->
new Digest options, checker