diff --git a/lib/auth.js b/lib/auth.js index 0f1caf2ec..013f8d774 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,4 +1,5 @@ /* eslint prefer-spread: "off" */ +/* eslint prefer-rest-params: "off" */ 'use strict'; @@ -7,363 +8,442 @@ const jju = require('jju'); const Error = require('http-errors'); const Logger = require('./logger'); const load_plugins = require('./plugin-loader').load_plugins; +const pkgJson = require('../package.json'); +/** + * Handles the authentification, load auth plugins. + */ +class Auth { -module.exports = Auth; + /** + * @param {*} config config reference + */ + constructor(config) { + this.config = config; + this.logger = Logger.logger.child({sub: 'auth'}); + this.secret = config.secret; -function Auth(config) { - let self = Object.create(Auth.prototype); - self.config = config; - self.logger = Logger.logger.child({sub: 'auth'}); - self.secret = config.secret; + const plugin_params = { + config: config, + logger: this.logger, + }; - let plugin_params = { - config: config, - logger: self.logger, - }; - - if (config.users_file) { - if (!config.auth || !config.auth.htpasswd) { - // b/w compat - config.auth = config.auth || {}; - config.auth.htpasswd = {file: config.users_file}; + if (config.users_file) { + if (!config.auth || !config.auth.htpasswd) { + // b/w compat + config.auth = config.auth || {}; + config.auth.htpasswd = {file: config.users_file}; + } } + + this.plugins = load_plugins(config, config.auth, plugin_params, function(p) { + return p.authenticate || p.allow_access || p.allow_publish; + }); + + this.plugins.unshift({ + verdaccio_version: pkgJson.version, + + 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]) + return cb( Error[403]('this user already exists') ); + + return cb(); + }, + }); + + const allow_action = function(action) { + return function(user, pkg, cb) { + let ok = pkg[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 ' + pkg.name) ); + } else { + cb( Error[403]('unregistered users are not allowed to ' + action + ' package ' + pkg.name) ); + } + }; + }; + + this.plugins.push({ + authenticate: function(user, password, cb) { + return cb( Error[403]('bad username/password, access denied') ); + }, + + add_user: function(user, password, cb) { + return cb( Error[409]('registration is disabled') ); + }, + + allow_access: allow_action('access'), + allow_publish: allow_action('publish'), + }); } - self.plugins = load_plugins(config, config.auth, plugin_params, function(p) { - return p.authenticate || p.allow_access || p.allow_publish; - }); + /** + * Authenticate an user. + * @param {*} user + * @param {*} password + * @param {*} cb + */ + authenticate(user, password, cb) { + const plugins = this.plugins.slice(0) + ;(function next() { + let p = plugins.shift(); - self.plugins.unshift({ - verdaccio_version: '1.1.0', - - 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]); + if (typeof(p.authenticate) !== 'function') { + return next(); } - return cb(); - }, + p.authenticate(user, password, function(err, groups) { + if (err) { + return cb(err); + } + if (groups != null && groups != false) { + return cb(err, authenticatedUser(user, groups)); + } + next(); + }); + })(); + } - adduser: function(user, password, cb) { - if (config.users && config.users[user]) - return cb( Error[403]('this user already exists') ); + /** + * Add a new user. + * @param {*} user + * @param {*} password + * @param {*} cb + */ + add_user(user, password, cb) { + let self = this; + let plugins = this.plugins.slice(0) - return cb(); - }, - }); - - function allow_action(action) { - return function(user, pkg, cb) { - let ok = pkg[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 ' + pkg.name) ); + ;(function next() { + let p = plugins.shift(); + let n = 'adduser'; + if (typeof(p[n]) !== 'function') { + n = 'add_user'; + } + if (typeof(p[n]) !== 'function') { + next(); } else { - cb( Error[403]('unregistered users are not allowed to ' + action + ' package ' + pkg.name) ); + p[n](user, password, function(err, ok) { + if (err) return cb(err); + if (ok) return self.authenticate(user, password, cb); + next(); + }); } + })(); + } + + /** + * Allow user to access a package. + * @param {*} package_name + * @param {*} user + * @param {*} callback + */ + allow_access(package_name, user, callback) { + let plugins = this.plugins.slice(0); + let pkg = Object.assign({name: package_name}, + this.config.get_package_spec(package_name)) + + ;(function next() { + let p = plugins.shift(); + + if (typeof(p.allow_access) !== 'function') { + return next(); + } + + p.allow_access(user, pkg, function(err, ok) { + if (err) return callback(err); + if (ok) return callback(null, ok); + next(); // cb(null, false) causes next plugin to roll + }); + })(); + } + + /** + * Allow user to publish a package. + * @param {*} package_name + * @param {*} user + * @param {*} callback + */ + allow_publish(package_name, user, callback) { + let plugins = this.plugins.slice(0); + let pkg = Object.assign({name: package_name}, + this.config.get_package_spec(package_name)) + + ;(function next() { + let p = plugins.shift(); + + if (typeof(p.allow_publish) !== 'function') { + return next(); + } + + p.allow_publish(user, pkg, function(err, ok) { + if (err) return callback(err); + if (ok) return callback(null, ok); + next(); // cb(null, false) causes next plugin to roll + }); + })(); + } + + /** + * Set up a basic middleware. + * @return {Function} + */ + basic_middleware() { + let self = this; + let credentials; + return function(req, res, _next) { + req.pause(); + + const next = function(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 = buildAnonymousUser(); + + let authorization = req.headers.authorization; + if (authorization == null) return next(); + + let parts = authorization.split(' '); + + if (parts.length !== 2) { + return next( Error[400]('bad authorization header') ); + } + + const scheme = parts[0]; + if (scheme === 'Basic') { + credentials = new Buffer(parts[1], 'base64').toString(); + } else if (scheme === 'Bearer') { + credentials = self.aes_decrypt(new Buffer(parts[1], 'base64')).toString('utf8'); + if (!credentials) { + return next(); + } + } else { + return next(); + } + + const index = credentials.indexOf(':'); + if (index < 0) { + return next(); + } + + const user = credentials.slice(0, index); + const pass = credentials.slice(index + 1); + + self.authenticate(user, pass, function(err, user) { + if (!err) { + req.remote_user = user; + next(); + } else { + req.remote_user = buildAnonymousUser(); + next(err); + } + }); }; } - self.plugins.push({ - authenticate: function(user, password, cb) { - return cb( Error[403]('bad username/password, access denied') ); - }, + /** + * Set up the bearer middleware. + * @return {Function} + */ + bearer_middleware() { + let self = this; + return function(req, res, _next) { + req.pause(); + const next = function(_err) { + req.resume(); + return _next.apply(null, arguments); + }; - add_user: function(user, password, cb) { - return cb( Error[409]('registration is disabled') ); - }, + if (req.remote_user != null && req.remote_user.name !== undefined) { + return next(); + } + req.remote_user = buildAnonymousUser(); - allow_access: allow_action('access'), - allow_publish: allow_action('publish'), - }); + let authorization = req.headers.authorization; + if (authorization == null) { + return next(); + } - return self; + let parts = authorization.split(' '); + + if (parts.length !== 2) { + return next( Error[400]('bad authorization header') ); + } + + let scheme = parts[0]; + let token = parts[1]; + + if (scheme !== 'Bearer') + return next(); + let user; + try { + user = self.decode_token(token); + } catch(err) { + return next(err); + } + + req.remote_user = authenticatedUser(user.u, user.g); + req.remote_user.token = token; + next(); + }; + } + + /** + * Set up cookie middleware. + * @return {Function} + */ + cookie_middleware() { + let self = this; + return function(req, res, _next) { + req.pause(); + const next = function(_err) { + req.resume(); + return _next(); + }; + + if (req.remote_user != null && req.remote_user.name !== undefined) + return next(); + + req.remote_user = buildAnonymousUser(); + + let token = req.cookies.get('token'); + if (token == null) { + return next(); + } + let credentials = self.aes_decrypt(new Buffer(token, 'base64')).toString('utf8'); + if (!credentials) { + return next(); + } + + let index = credentials.indexOf(':'); + if (index < 0) { + return next(); + } + const user = credentials.slice(0, index); + const pass = credentials.slice(index + 1); + + self.authenticate(user, pass, function(err, user) { + if (!err) { + req.remote_user = user; + next(); + } else { + req.remote_user = buildAnonymousUser(); + next(err); + } + }); + }; + } + + /** + * Generates the token. + * @param {*} user + * @return {String} + */ + issue_token(user) { + let 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 = new Buffer(data, 'utf8'); + const mac = Crypto.createHmac('sha256', this.secret).update(data).digest(); + return Buffer.concat([data, mac]).toString('base64'); + } + + /** + * Decodes the token. + * @param {*} str + * @param {*} expire_time + * @return {Object} + */ + decode_token(str, expire_time) { + const buf = new Buffer(str, 'base64'); + if (buf.length <= 32) { + throw Error[401]('invalid token'); + } + + let data = buf.slice(0, buf.length - 32); + let their_mac = buf.slice(buf.length - 32); + let 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; + } + + /** + * Encrypt a string. + * @param {String} buf + * @return {Buffer} + */ + aes_encrypt(buf) { + const c = Crypto.createCipher('aes192', this.secret); + const b1 = c.update(buf); + const b2 = c.final(); + return Buffer.concat([b1, b2]); + } + + /** + * Dencrypt a string. + * @param {String} buf + * @return {Buffer} + */ + aes_decrypt(buf) { + try { + const c = Crypto.createDecipher('aes192', this.secret); + const b1 = c.update(buf); + const b2 = c.final(); + return Buffer.concat([b1, b2]); + } catch(_) { + return new Buffer(0); + } + } } -Auth.prototype.authenticate = function(user, password, cb) { - let plugins = this.plugins.slice(0) - - ;(function next() { - let p = plugins.shift(); - - if (typeof(p.authenticate) !== 'function') { - return next(); - } - - p.authenticate(user, password, function(err, groups) { - if (err) return cb(err); - if (groups != null && groups != false) - return cb(err, authenticatedUser(user, groups)); - next(); - }); - })(); -}; - -Auth.prototype.add_user = function(user, password, cb) { - let self = this; - let plugins = this.plugins.slice(0) - - ;(function next() { - let p = plugins.shift(); - let n = 'adduser'; - if (typeof(p[n]) !== 'function') { - n = 'add_user'; - } - if (typeof(p[n]) !== 'function') { - next(); - } else { - p[n](user, password, function(err, ok) { - if (err) return cb(err); - if (ok) return self.authenticate(user, password, cb); - next(); - }); - } - })(); -}; - -Auth.prototype.allow_access = function(package_name, user, callback) { - let plugins = this.plugins.slice(0); - let pkg = Object.assign({name: package_name}, - this.config.get_package_spec(package_name)) - - ;(function next() { - let p = plugins.shift(); - - if (typeof(p.allow_access) !== 'function') { - return next(); - } - - p.allow_access(user, pkg, 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) { - let plugins = this.plugins.slice(0); - let pkg = Object.assign({name: package_name}, - this.config.get_package_spec(package_name)) - - ;(function next() { - let p = plugins.shift(); - - if (typeof(p.allow_publish) !== 'function') { - return next(); - } - - p.allow_publish(user, pkg, 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.basic_middleware = function() { - let 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 = buildAnonymousUser(); - - let authorization = req.headers.authorization; - if (authorization == null) return next(); - - let parts = authorization.split(' '); - - if (parts.length !== 2) - return next( Error[400]('bad authorization header') ); - - let scheme = parts[0]; - if (scheme === 'Basic') { - var credentials = new Buffer(parts[1], 'base64').toString(); - } else if (scheme === 'Bearer') { - var credentials = self.aes_decrypt(new Buffer(parts[1], 'base64')).toString('utf8'); - if (!credentials) return next(); - } else { - return next(); - } - - let index = credentials.indexOf(':'); - if (index < 0) return next(); - - let user = credentials.slice(0, index); - let pass = credentials.slice(index + 1); - - self.authenticate(user, pass, function(err, user) { - if (!err) { - req.remote_user = user; - next(); - } else { - req.remote_user = buildAnonymousUser(); - next(err); - } - }); - }; -}; - -Auth.prototype.bearer_middleware = function() { - let self = this; - return function(req, res, _next) { - req.pause(); - function next(_err) { - req.resume(); - return _next.apply(null, arguments); - } - - if (req.remote_user != null && req.remote_user.name !== undefined) - return next(); - req.remote_user = buildAnonymousUser(); - - let authorization = req.headers.authorization; - if (authorization == null) return next(); - - let parts = authorization.split(' '); - - if (parts.length !== 2) - return next( Error[400]('bad authorization header') ); - - let scheme = parts[0]; - let token = parts[1]; - - if (scheme !== 'Bearer') - return next(); - - try { - var user = self.decode_token(token); - } catch(err) { - return next(err); - } - - req.remote_user = authenticatedUser(user.u, user.g); - req.remote_user.token = token; - next(); - }; -}; - -Auth.prototype.cookie_middleware = function() { - let self = this; - return function(req, res, _next) { - req.pause(); - function next(_err) { - req.resume(); - return _next(); - } - - if (req.remote_user != null && req.remote_user.name !== undefined) - return next(); - - req.remote_user = buildAnonymousUser(); - - let token = req.cookies.get('token'); - if (token == null) return next(); - - /* try { - var user = self.decode_token(token, 60*60) - } catch(err) { - return next() - } - - req.remote_user = authenticatedUser(user.u, user.g) - req.remote_user.token = token - next()*/ - let credentials = self.aes_decrypt(new Buffer(token, 'base64')).toString('utf8'); - if (!credentials) return next(); - - let index = credentials.indexOf(':'); - if (index < 0) return next(); - - let user = credentials.slice(0, index); - let pass = credentials.slice(index + 1); - - self.authenticate(user, pass, function(err, user) { - if (!err) { - req.remote_user = user; - next(); - } else { - req.remote_user = buildAnonymousUser(); - next(err); - } - }); - }; -}; - -Auth.prototype.issue_token = function(user) { - let 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 = new Buffer(data, 'utf8'); - let mac = Crypto.createHmac('sha256', this.secret).update(data).digest(); - return Buffer.concat([data, mac]).toString('base64'); -}; - -Auth.prototype.decode_token = function(str, expire_time) { - let buf = new Buffer(str, 'base64'); - if (buf.length <= 32) throw Error[401]('invalid token'); - - let data = buf.slice(0, buf.length - 32); - let their_mac = buf.slice(buf.length - 32); - let 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; -}; - -Auth.prototype.aes_encrypt = function(buf) { - let c = Crypto.createCipher('aes192', this.secret); - let b1 = c.update(buf); - let b2 = c.final(); - return Buffer.concat([b1, b2]); -}; - -Auth.prototype.aes_decrypt = function(buf) { - try { - let c = Crypto.createDecipher('aes192', this.secret); - let b1 = c.update(buf); - let b2 = c.final(); - return Buffer.concat([b1, b2]); - } catch(_) { - return new Buffer(0); - } -}; - +/** + * Builds an anonymous user in case none is logged in. + * @return {Object} { name: xx, groups: [], real_groups: [] } + */ function buildAnonymousUser() { return { name: undefined, @@ -373,6 +453,12 @@ function buildAnonymousUser() { }; } +/** + * Authenticate an user. + * @param {*} name + * @param {*} groups + * @return {Object} { name: xx, groups: [], real_groups: [] } + */ function authenticatedUser(name, groups) { let _groups = (groups || []).concat(['$all', '$authenticated', '@all', '@authenticated', 'all']); return { @@ -381,3 +467,5 @@ function authenticatedUser(name, groups) { real_groups: groups, }; } + +module.exports = Auth; diff --git a/lib/index.js b/lib/index.js index 5be203e43..11870d355 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,14 +15,14 @@ module.exports = function(config_hash) { let config = new Config(config_hash); let storage = new Storage(config); - let auth = Auth(config); + let auth = new Auth(config); let app = express(); // run in production mode by default, just in case // it shouldn't make any difference anyway app.set('env', process.env.NODE_ENV || 'production'); - function error_reporting_middleware(req, res, next) { + const error_reporting_middleware = function(req, res, next) { res.report_error = res.report_error || function(err) { if (err.status && err.status >= 400 && err.status < 600) { if (!res.headersSent) { @@ -44,7 +44,7 @@ module.exports = function(config_hash) { } }; next(); - } + }; app.use(Middleware.log); app.use(error_reporting_middleware);