2015-02-24 14:28:16 -05:00
|
|
|
var Crypto = require('crypto')
|
|
|
|
var jju = require('jju')
|
|
|
|
var Error = require('http-errors')
|
|
|
|
var Logger = require('./logger')
|
|
|
|
var load_plugins = require('./plugin-loader').load_plugins
|
2014-09-01 18:09:08 -05:00
|
|
|
|
|
|
|
module.exports = Auth
|
|
|
|
|
|
|
|
function Auth(config) {
|
2014-11-12 06:14:37 -05:00
|
|
|
var self = Object.create(Auth.prototype)
|
|
|
|
self.config = config
|
|
|
|
self.logger = Logger.logger.child({ sub: 'auth' })
|
2014-11-24 14:46:37 -05:00
|
|
|
self.secret = config.secret
|
2014-11-12 06:14:37 -05:00
|
|
|
|
2015-02-24 14:28:16 -05:00
|
|
|
var plugin_params = {
|
2014-11-12 06:14:37 -05:00
|
|
|
config: config,
|
2015-02-24 14:28:16 -05:00
|
|
|
logger: self.logger
|
2014-11-12 06:14:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (config.users_file) {
|
|
|
|
if (!config.auth || !config.auth.htpasswd) {
|
|
|
|
// b/w compat
|
|
|
|
config.auth = config.auth || {}
|
|
|
|
config.auth.htpasswd = { file: config.users_file }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-08 15:54:59 -05:00
|
|
|
self.plugins = load_plugins(config, config.auth, plugin_params, function (p) {
|
|
|
|
return p.authenticate || p.allow_access || p.allow_publish
|
|
|
|
})
|
2014-11-12 06:14:37 -05:00
|
|
|
|
|
|
|
self.plugins.unshift({
|
2015-04-08 15:54:59 -05:00
|
|
|
sinopia_version: '1.1.0',
|
|
|
|
|
2014-11-12 06:14:37 -05:00
|
|
|
authenticate: function(user, password, cb) {
|
|
|
|
if (config.users != null
|
|
|
|
&& config.users[user] != null
|
|
|
|
&& (Crypto.createHash('sha1').update(password).digest('hex')
|
|
|
|
=== config.users[user].password)
|
|
|
|
) {
|
|
|
|
return cb(null, [ user ])
|
|
|
|
}
|
|
|
|
|
|
|
|
return cb()
|
|
|
|
},
|
|
|
|
|
|
|
|
adduser: function(user, password, cb) {
|
|
|
|
if (config.users && config.users[user])
|
2014-11-24 20:24:18 -05:00
|
|
|
return cb( Error[403]('this user already exists') )
|
2014-11-12 06:14:37 -05:00
|
|
|
|
|
|
|
return cb()
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2015-04-08 15:54:59 -05:00
|
|
|
function allow_action(action) {
|
|
|
|
return function(user, package, cb) {
|
|
|
|
var ok = package[action].reduce(function(prev, curr) {
|
|
|
|
if (user.groups.indexOf(curr) !== -1) return true
|
|
|
|
return prev
|
|
|
|
}, false)
|
|
|
|
|
|
|
|
if (ok) return cb(null, true)
|
|
|
|
|
|
|
|
if (user.name) {
|
|
|
|
cb( Error[403]('user ' + user.name + ' is not allowed to ' + action + ' package ' + package.name) )
|
|
|
|
} else {
|
|
|
|
cb( Error[403]('unregistered users are not allowed to ' + action + ' package ' + package.name) )
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-12 06:14:37 -05:00
|
|
|
self.plugins.push({
|
|
|
|
authenticate: function(user, password, cb) {
|
|
|
|
return cb( Error[403]('bad username/password, access denied') )
|
|
|
|
},
|
|
|
|
|
2015-04-08 15:54:59 -05:00
|
|
|
add_user: function(user, password, cb) {
|
2014-11-12 06:14:37 -05:00
|
|
|
return cb( Error[409]('registration is disabled') )
|
|
|
|
},
|
2015-04-08 15:54:59 -05:00
|
|
|
|
|
|
|
allow_access: allow_action('access'),
|
|
|
|
allow_publish: allow_action('publish'),
|
2014-11-12 06:14:37 -05:00
|
|
|
})
|
|
|
|
|
|
|
|
return self
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.authenticate = function(user, password, cb) {
|
2014-11-12 06:14:37 -05:00
|
|
|
var plugins = this.plugins.slice(0)
|
|
|
|
|
|
|
|
;(function next() {
|
|
|
|
var p = plugins.shift()
|
2015-04-08 15:54:59 -05:00
|
|
|
|
|
|
|
if (typeof(p.authenticate) !== 'function') {
|
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
2014-11-12 06:14:37 -05:00
|
|
|
p.authenticate(user, password, function(err, groups) {
|
2014-11-16 07:37:50 -05:00
|
|
|
if (err) return cb(err)
|
|
|
|
if (groups != null && groups != false)
|
|
|
|
return cb(err, AuthenticatedUser(user, groups))
|
2014-11-12 06:14:37 -05:00
|
|
|
next()
|
|
|
|
})
|
|
|
|
})()
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.add_user = function(user, password, cb) {
|
2014-11-16 07:37:50 -05:00
|
|
|
var self = this
|
2014-11-12 06:14:37 -05:00
|
|
|
var plugins = this.plugins.slice(0)
|
|
|
|
|
|
|
|
;(function next() {
|
|
|
|
var p = plugins.shift()
|
|
|
|
var n = 'adduser'
|
|
|
|
if (typeof(p[n]) !== 'function') {
|
|
|
|
n = 'add_user'
|
|
|
|
}
|
|
|
|
if (typeof(p[n]) !== 'function') {
|
|
|
|
next()
|
|
|
|
} else {
|
|
|
|
p[n](user, password, function(err, ok) {
|
2014-11-16 07:37:50 -05:00
|
|
|
if (err) return cb(err)
|
|
|
|
if (ok) return self.authenticate(user, password, cb)
|
2014-11-12 06:14:37 -05:00
|
|
|
next()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})()
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|
2015-04-08 15:54:59 -05:00
|
|
|
Auth.prototype.allow_access = function(package_name, user, callback) {
|
|
|
|
var plugins = this.plugins.slice(0)
|
|
|
|
var package = Object.assign({ name: package_name },
|
|
|
|
this.config.get_package_spec(package_name))
|
|
|
|
|
|
|
|
;(function next() {
|
|
|
|
var p = plugins.shift()
|
|
|
|
|
|
|
|
if (typeof(p.allow_access) !== 'function') {
|
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
|
|
|
p.allow_access(user, package, function(err, ok) {
|
|
|
|
if (err) return callback(err)
|
|
|
|
if (ok) return callback(null, ok)
|
|
|
|
next() // cb(null, false) causes next plugin to roll
|
|
|
|
})
|
|
|
|
})()
|
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.allow_publish = function(package_name, user, callback) {
|
|
|
|
var plugins = this.plugins.slice(0)
|
|
|
|
var package = Object.assign({ name: package_name },
|
|
|
|
this.config.get_package_spec(package_name))
|
|
|
|
|
|
|
|
;(function next() {
|
|
|
|
var p = plugins.shift()
|
|
|
|
|
2015-04-21 11:41:50 -05:00
|
|
|
if (typeof(p.allow_publish) !== 'function') {
|
2015-04-08 15:54:59 -05:00
|
|
|
return next()
|
|
|
|
}
|
|
|
|
|
2015-04-21 11:41:50 -05:00
|
|
|
p.allow_publish(user, package, function(err, ok) {
|
2015-04-08 15:54:59 -05:00
|
|
|
if (err) return callback(err)
|
|
|
|
if (ok) return callback(null, ok)
|
|
|
|
next() // cb(null, false) causes next plugin to roll
|
|
|
|
})
|
|
|
|
})()
|
|
|
|
}
|
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
Auth.prototype.basic_middleware = function() {
|
2014-11-12 06:14:37 -05:00
|
|
|
var self = this
|
|
|
|
return function(req, res, _next) {
|
|
|
|
req.pause()
|
|
|
|
function next(err) {
|
|
|
|
req.resume()
|
|
|
|
// uncomment this to reject users with bad auth headers
|
|
|
|
//return _next.apply(null, arguments)
|
|
|
|
|
|
|
|
// swallow error, user remains unauthorized
|
|
|
|
// set remoteUserError to indicate that user was attempting authentication
|
|
|
|
if (err) req.remote_user.error = err.message
|
|
|
|
return _next()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.remote_user != null && req.remote_user.name !== undefined)
|
|
|
|
return next()
|
|
|
|
req.remote_user = AnonymousUser()
|
|
|
|
|
|
|
|
var authorization = req.headers.authorization
|
|
|
|
if (authorization == null) return next()
|
|
|
|
|
|
|
|
var parts = authorization.split(' ')
|
|
|
|
|
|
|
|
if (parts.length !== 2)
|
|
|
|
return next( Error[400]('bad authorization header') )
|
|
|
|
|
|
|
|
var scheme = parts[0]
|
2014-11-24 14:46:37 -05:00
|
|
|
if (scheme === 'Basic') {
|
|
|
|
var credentials = Buffer(parts[1], 'base64').toString()
|
|
|
|
} else if (scheme === 'Bearer') {
|
|
|
|
var credentials = self.aes_decrypt(Buffer(parts[1], 'base64')).toString('utf8')
|
|
|
|
if (!credentials) return next()
|
|
|
|
} else {
|
2014-11-16 07:37:50 -05:00
|
|
|
return next()
|
2014-11-24 14:46:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
var index = credentials.indexOf(':')
|
|
|
|
if (index < 0) return next()
|
2014-11-12 06:14:37 -05:00
|
|
|
|
|
|
|
var user = credentials.slice(0, index)
|
|
|
|
var pass = credentials.slice(index + 1)
|
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
self.authenticate(user, pass, function(err, user) {
|
|
|
|
if (!err) {
|
|
|
|
req.remote_user = user
|
2014-11-12 06:14:37 -05:00
|
|
|
next()
|
|
|
|
} else {
|
|
|
|
req.remote_user = AnonymousUser()
|
|
|
|
next(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
Auth.prototype.bearer_middleware = function() {
|
2014-11-12 06:14:37 -05:00
|
|
|
var self = this
|
2014-11-16 07:37:50 -05:00
|
|
|
return function(req, res, _next) {
|
|
|
|
req.pause()
|
2015-03-28 13:25:53 -05:00
|
|
|
function next(_err) {
|
2014-11-16 07:37:50 -05:00
|
|
|
req.resume()
|
|
|
|
return _next.apply(null, arguments)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.remote_user != null && req.remote_user.name !== undefined)
|
|
|
|
return next()
|
|
|
|
req.remote_user = AnonymousUser()
|
|
|
|
|
|
|
|
var authorization = req.headers.authorization
|
|
|
|
if (authorization == null) return next()
|
|
|
|
|
|
|
|
var parts = authorization.split(' ')
|
|
|
|
|
|
|
|
if (parts.length !== 2)
|
|
|
|
return next( Error[400]('bad authorization header') )
|
|
|
|
|
|
|
|
var scheme = parts[0]
|
|
|
|
var token = parts[1]
|
|
|
|
|
|
|
|
if (scheme !== 'Bearer')
|
|
|
|
return next()
|
|
|
|
|
|
|
|
try {
|
|
|
|
var user = self.decode_token(token)
|
|
|
|
} catch(err) {
|
|
|
|
return next(err)
|
|
|
|
}
|
2014-11-12 06:14:37 -05:00
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
req.remote_user = AuthenticatedUser(user.u, user.g)
|
|
|
|
req.remote_user.token = token
|
|
|
|
next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.cookie_middleware = function() {
|
|
|
|
var self = this
|
2014-11-12 06:14:37 -05:00
|
|
|
return function(req, res, _next) {
|
|
|
|
req.pause()
|
2015-03-28 13:25:53 -05:00
|
|
|
function next(_err) {
|
2014-11-12 06:14:37 -05:00
|
|
|
req.resume()
|
|
|
|
return _next()
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.remote_user != null && req.remote_user.name !== undefined)
|
|
|
|
return next()
|
|
|
|
|
|
|
|
req.remote_user = AnonymousUser()
|
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
var token = req.cookies.get('token')
|
|
|
|
if (token == null) return next()
|
2014-11-12 06:14:37 -05:00
|
|
|
|
2014-11-24 14:46:37 -05:00
|
|
|
/*try {
|
|
|
|
var user = self.decode_token(token, 60*60)
|
2014-11-16 07:37:50 -05:00
|
|
|
} catch(err) {
|
|
|
|
return next()
|
|
|
|
}
|
2014-11-12 06:14:37 -05:00
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
req.remote_user = AuthenticatedUser(user.u, user.g)
|
|
|
|
req.remote_user.token = token
|
2014-11-24 14:46:37 -05:00
|
|
|
next()*/
|
|
|
|
var credentials = self.aes_decrypt(Buffer(token, 'base64')).toString('utf8')
|
|
|
|
if (!credentials) return next()
|
|
|
|
|
|
|
|
var index = credentials.indexOf(':')
|
|
|
|
if (index < 0) return next()
|
|
|
|
|
|
|
|
var user = credentials.slice(0, index)
|
|
|
|
var pass = credentials.slice(index + 1)
|
|
|
|
|
|
|
|
self.authenticate(user, pass, function(err, user) {
|
|
|
|
if (!err) {
|
|
|
|
req.remote_user = user
|
|
|
|
next()
|
|
|
|
} else {
|
|
|
|
req.remote_user = AnonymousUser()
|
|
|
|
next(err)
|
|
|
|
}
|
|
|
|
})
|
2014-11-12 06:14:37 -05:00
|
|
|
}
|
2014-11-04 09:47:03 -05:00
|
|
|
}
|
|
|
|
|
2014-11-16 07:37:50 -05:00
|
|
|
Auth.prototype.issue_token = function(user) {
|
|
|
|
var data = jju.stringify({
|
|
|
|
u: user.name,
|
|
|
|
g: user.real_groups && user.real_groups.length ? user.real_groups : undefined,
|
|
|
|
t: ~~(Date.now()/1000),
|
|
|
|
}, { indent: false })
|
|
|
|
|
|
|
|
data = Buffer(data, 'utf8')
|
|
|
|
var mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
|
|
|
|
return Buffer.concat([ data, mac ]).toString('base64')
|
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.decode_token = function(str, expire_time) {
|
|
|
|
var buf = Buffer(str, 'base64')
|
|
|
|
if (buf.length <= 32) throw Error[401]('invalid token')
|
|
|
|
|
|
|
|
var data = buf.slice(0, buf.length - 32)
|
|
|
|
var their_mac = buf.slice(buf.length - 32)
|
|
|
|
var good_mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
|
|
|
|
|
|
|
|
their_mac = Crypto.createHash('sha512').update(their_mac).digest('hex')
|
|
|
|
good_mac = Crypto.createHash('sha512').update(good_mac).digest('hex')
|
|
|
|
if (their_mac !== good_mac) throw Error[401]('bad signature')
|
|
|
|
|
|
|
|
// make token expire in 24 hours
|
|
|
|
// TODO: make configurable?
|
|
|
|
expire_time = expire_time || 24*60*60
|
|
|
|
|
|
|
|
data = jju.parse(data.toString('utf8'))
|
|
|
|
if (Math.abs(data.t - ~~(Date.now()/1000)) > expire_time)
|
|
|
|
throw Error[401]('token expired')
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2014-11-24 14:46:37 -05:00
|
|
|
Auth.prototype.aes_encrypt = function(buf) {
|
|
|
|
var c = Crypto.createCipher('aes192', this.secret)
|
|
|
|
var b1 = c.update(buf)
|
|
|
|
var b2 = c.final()
|
|
|
|
return Buffer.concat([ b1, b2 ])
|
|
|
|
}
|
|
|
|
|
|
|
|
Auth.prototype.aes_decrypt = function(buf) {
|
|
|
|
try {
|
|
|
|
var c = Crypto.createDecipher('aes192', this.secret)
|
|
|
|
var b1 = c.update(buf)
|
|
|
|
var b2 = c.final()
|
|
|
|
} catch(_) {
|
|
|
|
return Buffer(0)
|
|
|
|
}
|
|
|
|
return Buffer.concat([ b1, b2 ])
|
|
|
|
}
|
|
|
|
|
2014-09-01 18:09:08 -05:00
|
|
|
function AnonymousUser() {
|
2014-11-12 06:14:37 -05:00
|
|
|
return {
|
|
|
|
name: undefined,
|
2015-02-24 14:28:16 -05:00
|
|
|
// groups without '$' are going to be deprecated eventually
|
2014-11-12 06:14:37 -05:00
|
|
|
groups: [ '$all', '$anonymous', '@all', '@anonymous', 'all', 'undefined', 'anonymous' ],
|
2014-11-16 07:37:50 -05:00
|
|
|
real_groups: [],
|
2014-11-12 06:14:37 -05:00
|
|
|
}
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function AuthenticatedUser(name, groups) {
|
2014-11-16 07:37:50 -05:00
|
|
|
var _groups = (groups || []).concat([ '$all', '$authenticated', '@all', '@authenticated', 'all' ])
|
2014-11-12 06:14:37 -05:00
|
|
|
return {
|
|
|
|
name: name,
|
2014-11-16 07:37:50 -05:00
|
|
|
groups: _groups,
|
|
|
|
real_groups: groups,
|
2014-11-12 06:14:37 -05:00
|
|
|
}
|
2014-09-01 18:09:08 -05:00
|
|
|
}
|
|
|
|
|