diff --git a/package-lock.json b/package-lock.json index 3a2b0e572..c5a9b7cdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3487,36 +3487,6 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, - "passport": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", - "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", - "requires": { - "passport-strategy": "1.x.x", - "pause": "0.0.1" - } - }, - "passport-http-bearer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz", - "integrity": "sha1-FHRp6jZp4qhMYWfvmdu3fh8AmKg=", - "requires": { - "passport-strategy": "1.x.x" - } - }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", - "requires": { - "passport-strategy": "1.x.x" - } - }, - "passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" - }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -3567,11 +3537,6 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, - "pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" - }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index bd7c035d0..7214728f8 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,6 @@ "nodemailer-smtp-transport": "^2.7.4", "once": "^1.4.0", "parse-links": "^0.1.0", - "passport": "^0.4.1", - "passport-http-bearer": "^1.0.1", - "passport-local": "^1.0.0", "pretty-bytes": "^5.3.0", "progress-stream": "^2.0.0", "proxy-middleware": "^0.15.0", diff --git a/src/accesscontrol.js b/src/accesscontrol.js index d2bc03628..46f092b9c 100644 --- a/src/accesscontrol.js +++ b/src/accesscontrol.js @@ -121,7 +121,7 @@ function validateToken(accessToken, callback) { tokendb.getByAccessToken(accessToken, function (error, token) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401 - if (error) return callback(error); // this triggers 'internal error' in passport + if (error) return callback(error); users.get(token.identifier, function (error, user) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401 diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index 0a2133293..1b8247198 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -1,8 +1,8 @@ 'use strict'; exports = module.exports = { - initialize: initialize, - uninitialize: uninitialize, + passwordAuth: passwordAuth, + tokenAuth: tokenAuth, scope: scope, websocketAuth: websocketAuth @@ -10,83 +10,91 @@ exports = module.exports = { var accesscontrol = require('../accesscontrol.js'), assert = require('assert'), - BearerStrategy = require('passport-http-bearer').Strategy, BoxError = require('../boxerror.js'), externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, - LocalStrategy = require('passport-local').Strategy, - passport = require('passport'), users = require('../users.js'); -function initialize(callback) { - assert.strictEqual(typeof callback, 'function'); +function passwordAuth(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); - // serialize user into session - passport.serializeUser(function (user, callback) { - callback(null, user.id); - }); + if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string')); + if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'A password must be non-empty string')); - // deserialize user from session - passport.deserializeUser(function(userId, callback) { - users.get(userId, function (error, result) { - if (error) return callback(null, null /* user */, error.message); // will end up as a 401. can happen if user with active session got deleted + const username = req.body.username; + const password = req.body.password; - callback(null, result); + // TODO we should only do this for dashboard logins + function createAndVerifyUserIfNotExist(identifier, password) { + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof password, 'string'); + + externalLdap.createAndVerifyUserIfNotExist(identifier.toLowerCase(), password, function (error, result) { + if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(401, 'Unauthorized')); + if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(401, 'Unauthorized')); + if (error && error.reason === BoxError.CONFLICT) return next(new HttpError(401, 'Unauthorized')); + if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized')); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error)); + + req.user = result; + + next(); }); - }); + } - // used when username/password is sent in request body. used in CLI login & oauth2 login route - passport.use(new LocalStrategy(function (username, password, callback) { + if (username.indexOf('@') === -1) { + users.verifyWithUsername(username, password, users.AP_WEBADMIN, function (error, result) { + if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error)); + if (!result) return next(new HttpError(401, 'Unauthorized')); - // TODO we should only do this for dashboard logins - function createAndVerifyUserIfNotExist(identifier, password, callback) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof callback, 'function'); + req.user = result; - externalLdap.createAndVerifyUserIfNotExist(identifier.toLowerCase(), password, function (error, result) { - if (error && error.reason === BoxError.BAD_STATE) return callback(null, false); - if (error && error.reason === BoxError.BAD_FIELD) return callback(null, false); - if (error && error.reason === BoxError.CONFLICT) return callback(null, false); - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false); - if (error) return callback(error); + next(); + }); + } else { + users.verifyWithEmail(username, password, users.AP_WEBADMIN, function (error, result) { + if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password); + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error)); + if (!result) return next(new HttpError(401, 'Unauthorized')); - callback(null, result); - }); - } + req.user = result; - if (username.indexOf('@') === -1) { - users.verifyWithUsername(username, password, users.AP_WEBADMIN, function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password, callback); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false); - if (error) return callback(error); - if (!result) return callback(null, false); - callback(null, result); - }); - } else { - users.verifyWithEmail(username, password, users.AP_WEBADMIN, function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password, callback); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false); - if (error) return callback(error); - if (!result) return callback(null, false); - callback(null, result); - }); - } - })); - - // used for "Authorization: Bearer token" or access_token query param authentication - passport.use(new BearerStrategy(function (token, callback) { - accesscontrol.validateToken(token, callback); - })); - - callback(null); + next(); + }); + } } -function uninitialize(callback) { - assert.strictEqual(typeof callback, 'function'); +function tokenAuth(req, res, next) { + var token; - callback(null); + // this determines the priority + if (req.body && req.body.access_token) token = req.body.access_token; + if (req.query && req.query.access_token) token = req.query.access_token; + if (req.headers && req.headers.authorization) { + var parts = req.headers.authorization.split(' '); + if (parts.length == 2) { + var scheme = parts[0]; + var credentials = parts[1]; + + if (/^Bearer$/i.test(scheme)) token = credentials; + } + } + + if (!token) return next(new HttpError(401, 'Unauthorized')); + + accesscontrol.validateToken(token, function (error, user, info) { + if (error) return next(new HttpError(500, error.message)); + if (!user) return next(new HttpError(401, 'Unauthorized')); + + req.user = user; + req.authInfo = info; + + next(); + }); } // The scope middleware provides an auth middleware for routes. @@ -103,18 +111,14 @@ function scope(requiredScope) { var requiredScopes = requiredScope.split(','); - return [ - passport.authenticate(['bearer'], { session: false }), + return function (req, res, next) { + assert(req.authInfo && typeof req.authInfo === 'object'); - function (req, res, next) { - assert(req.authInfo && typeof req.authInfo === 'object'); + var error = accesscontrol.hasScopes(req.authInfo.authorizedScopes, requiredScopes); + if (error) return next(new HttpError(403, error.message)); - var error = accesscontrol.hasScopes(req.authInfo.authorizedScopes, requiredScopes); - if (error) return next(new HttpError(403, error.message)); - - next(); - } - ]; + next(); + }; } function websocketAuth(requiredScopes, req, res, next) { diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index b599ef9f6..8273a9473 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -34,7 +34,6 @@ let assert = require('assert'), externalLdap = require('../externalldap.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - passport = require('passport'), speakeasy = require('speakeasy'), sysinfo = require('../sysinfo.js'), system = require('../system.js'), @@ -44,26 +43,23 @@ let assert = require('assert'), updateChecker = require('../updatechecker.js'); function login(req, res, next) { - passport.authenticate('local', function (error, user) { + assert.strictEqual(typeof req.user, 'object'); + + var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; + + if (!req.user.ghost && !req.user.appPassword && req.user.twoFactorAuthenticationEnabled) { + if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided')); + + let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + if (!verified) return next(new HttpError(401, 'Invalid totpToken')); + } + + const auditSource = { authType: 'cli', ip: ip }; + clients.issueDeveloperToken(req.user, auditSource, function (error, result) { if (error) return next(new HttpError(500, error)); - if (!user) return next(new HttpError(401, 'Invalid credentials')); - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; - - if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided')); - - let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } - - const auditSource = { authType: 'cli', ip: ip }; - clients.issueDeveloperToken(user, auditSource, function (error, result) { - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, result)); - }); - })(req, res, next); + next(new HttpSuccess(200, result)); + }); } function logout(req, res) { diff --git a/src/routes/developer.js b/src/routes/developer.js index 724c5a229..7acf4f619 100644 --- a/src/routes/developer.js +++ b/src/routes/developer.js @@ -4,32 +4,29 @@ exports = module.exports = { login: login }; -var clients = require('../clients.js'), - passport = require('passport'), +let assert = require('assert'), + clients = require('../clients.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, speakeasy = require('speakeasy'); function login(req, res, next) { - passport.authenticate('local', function (error, user) { + assert.strictEqual(typeof req.user, 'object'); + + var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; + + if (!req.user.ghost && !req.user.appPassword && req.user.twoFactorAuthenticationEnabled) { + if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided')); + + let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + if (!verified) return next(new HttpError(401, 'Invalid totpToken')); + } + + const auditSource = { authType: 'cli', ip: ip }; + clients.issueDeveloperToken(req.user, auditSource, function (error, result) { if (error) return next(new HttpError(500, error)); - if (!user) return next(new HttpError(401, 'Invalid credentials')); - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; - - if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided')); - - let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } - - const auditSource = { authType: 'cli', ip: ip }; - clients.issueDeveloperToken(user, auditSource, function (error, result) { - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, result)); - }); - })(req, res, next); + next(new HttpSuccess(200, result)); + }); } diff --git a/src/routes/test/accesscontrol-test.js b/src/routes/test/accesscontrol-test.js index b18ae292e..0b8f062f5 100644 --- a/src/routes/test/accesscontrol-test.js +++ b/src/routes/test/accesscontrol-test.js @@ -1,30 +1,14 @@ /* jslint node:true */ /* global it:false */ /* global describe:false */ -/* global before:false */ -/* global after:false */ 'use strict'; var accesscontrol = require('../accesscontrol.js'), expect = require('expect.js'), - HttpError = require('connect-lastmile').HttpError, - passport = require('passport'); + HttpError = require('connect-lastmile').HttpError; describe('scopes middleware', function () { - var passportAuthenticateSave = null; - - before(function () { - passportAuthenticateSave = passport.authenticate; - passport.authenticate = function () { - return function (req, res, next) { next(); }; - }; - }); - - after(function () { - passport.authenticate = passportAuthenticateSave; - }); - it('fails due to empty scope in request', function (done) { var mw = accesscontrol.scope('admin')[1]; var req = { authInfo: { authorizedScopes: [ ] } }; diff --git a/src/server.js b/src/server.js index efd6dfdfd..96df77459 100644 --- a/src/server.js +++ b/src/server.js @@ -15,7 +15,6 @@ var accesscontrol = require('./accesscontrol.js'), express = require('express'), http = require('http'), middleware = require('./middleware'), - passport = require('passport'), routes = require('./routes/index.js'), settings = require('./settings.js'), ws = require('ws'); @@ -68,7 +67,6 @@ function initializeExpressSync() { .use(json) .use(urlencoded) .use(middleware.cors({ origins: [ '*' ], allowCredentials: false })) - .use(passport.initialize()) .use(router) .use(middleware.lastMile()); @@ -78,21 +76,24 @@ function initializeExpressSync() { var multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT }); + const password = routes.accesscontrol.passwordAuth; + const token = routes.accesscontrol.tokenAuth; + // scope middleware implicitly also adds bearer token verification - var cloudronScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLOUDRON); - var subscriptionScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SUBSCRIPTION); - var appstoreScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE); - var profileScope = routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE); - var usersReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_READ); - var usersManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_MANAGE); - var appsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_READ); - var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE) ]; - var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS); - var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL); - var notificationsScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE), routes.notifications.verifyOwnership ]; - var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS); - var domainsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_READ); - var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE); + var cloudronScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_CLOUDRON) ]; + var subscriptionScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_SUBSCRIPTION) ]; + var appstoreScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE) ]; + var profileScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE) ]; + var usersReadScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_READ) ]; + var usersManageScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_MANAGE) ]; + var appsReadScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_READ) ]; + var appsManageScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE) ]; + var settingsScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS) ]; + var mailScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL) ]; + var notificationsScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE), routes.notifications.verifyOwnership ]; + var clientsScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS) ]; + var domainsReadScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_READ) ]; + var domainsManageScope = [ token, routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE) ]; const verifyDomainLock = routes.domains.verifyDomainLock; @@ -105,14 +106,14 @@ function initializeExpressSync() { router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar // login/logout routes - router.post('/api/v1/cloudron/login', routes.cloudron.login); + router.post('/api/v1/cloudron/login', password, routes.cloudron.login); router.get ('/api/v1/cloudron/logout', routes.cloudron.logout); // this will invalidate the token if any and redirect to /login.html always router.post('/api/v1/cloudron/password_reset_request', routes.cloudron.passwordResetRequest); router.post('/api/v1/cloudron/password_reset', routes.cloudron.passwordReset); router.post('/api/v1/cloudron/setup_account', routes.cloudron.setupAccount); // developer routes - router.post('/api/v1/developer/login', routes.developer.login); + router.post('/api/v1/developer/login', password, routes.developer.login); // cloudron routes router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo); @@ -339,7 +340,6 @@ function start(callback) { gHttpServer = initializeExpressSync(); async.series([ - routes.accesscontrol.initialize, // hooks up authentication strategies into passport database.initialize, settings.initCache, // pre-load very often used settings cloudron.initialize, @@ -356,7 +356,6 @@ function stop(callback) { async.series([ cloudron.uninitialize, database.uninitialize, - routes.accesscontrol.uninitialize, gHttpServer.close.bind(gHttpServer), ], function (error) { if (error) return callback(error);