From ac36a99a7226e1092c2e7e28c9b6e32d8f82e6fb Mon Sep 17 00:00:00 2001 From: Egil Moeller Date: Thu, 19 Apr 2012 14:25:12 +0200 Subject: [PATCH] More general basic auth --- settings.json.template | 25 ++++-- src/node/hooks/express/adminplugins.js | 2 + src/node/hooks/express/socketio.js | 18 +++- src/node/hooks/express/webaccess.js | 110 +++++++++++++++++-------- src/node/utils/Settings.js | 15 ++-- src/package.json | 1 + 6 files changed, 123 insertions(+), 48 deletions(-) diff --git a/settings.json.template b/settings.json.template index a664ea8ed..d8f67e765 100644 --- a/settings.json.template +++ b/settings.json.template @@ -46,12 +46,27 @@ /* This is the path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to enable the import/export of pads*/ "abiword" : null, - - /* This setting is used if you need http basic auth */ - // "httpAuth" : "user:pass", + + /* This setting is used if you require authentication of all users. + Note: /admin always requires authentication. */ + "requireAuthentication": false, - /* This setting is used for http basic auth for admin pages. If not set, the admin page won't be accessible from web*/ - // "adminHttpAuth" : "user:pass", + /* Require authorization by a module, or a user with is_admin set, + see below. Access to /admin allways requires either, regardless + of this setting. */ + "requireAuthorization": false, + + /* Users for basic authentication. is_admin = true gives access to /admin */ + "users": { + "admin": { + "password": "changeme", + "is_admin": true + }, + "user": { + "password": "changeme", + "is_admin": false + } + }, /* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */ "loglevel": "INFO" diff --git a/src/node/hooks/express/adminplugins.js b/src/node/hooks/express/adminplugins.js index 1d1320ab3..6cc80cf2a 100644 --- a/src/node/hooks/express/adminplugins.js +++ b/src/node/hooks/express/adminplugins.js @@ -21,6 +21,8 @@ exports.expressCreateServer = function (hook_name, args, cb) { exports.socketio = function (hook_name, args, cb) { var io = args.io.of("/pluginfw/installer"); io.on('connection', function (socket) { + if (!socket.handshake.session.user.is_admin) return; + socket.on("load", function (query) { socket.emit("installed-results", {results: plugins.plugins}); }); diff --git a/src/node/hooks/express/socketio.js b/src/node/hooks/express/socketio.js index e040f7aca..6774b653a 100644 --- a/src/node/hooks/express/socketio.js +++ b/src/node/hooks/express/socketio.js @@ -7,11 +7,27 @@ var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var padMessageHandler = require("../../handler/PadMessageHandler"); var timesliderMessageHandler = require("../../handler/TimesliderMessageHandler"); - +var connect = require('connect'); + exports.expressCreateServer = function (hook_name, args, cb) { //init socket.io and redirect all requests to the MessageHandler var io = socketio.listen(args.app); + /* Require an express session cookie to be present, and load the + * session. See http://www.danielbaulig.de/socket-ioexpress for more + * info */ + io.set('authorization', function (data, accept) { + if (!data.headers.cookie) return accept('No session cookie transmitted.', false); + data.cookie = connect.utils.parseCookie(data.headers.cookie); + data.sessionID = data.cookie.express_sid; + args.app.sessionStore.get(data.sessionID, function (err, session) { + if (err || !session) return accept('Bad session / session has expired', false); + data.session = new connect.middleware.session.Session(data, session); + accept(null, true); + }); + }); + + //this is only a workaround to ensure it works with all browers behind a proxy //we should remove this when the new socket.io version is more stable io.set('transports', ['xhr-polling']); diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js index 48b5edae7..499451d89 100644 --- a/src/node/hooks/express/webaccess.js +++ b/src/node/hooks/express/webaccess.js @@ -2,55 +2,99 @@ var express = require('express'); var log4js = require('log4js'); var httpLogger = log4js.getLogger("http"); var settings = require('../../utils/Settings'); +var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); //checks for basic http auth exports.basicAuth = function (req, res, next) { - - // When handling HTTP-Auth, an undefined password will lead to no authorization at all - var pass = settings.httpAuth || ''; - - if (req.path.indexOf('/admin') == 0) { - var pass = settings.adminHttpAuth; - + var authorize = function (cb) { + // Do not require auth for static paths...this could be a bit brittle + if (req.path.match(/^\/(static|javascripts|pluginfw)/)) return cb(true); + + if (req.path.indexOf('/admin') != 0) { + if (!settings.requireAuthentication) return cb(true); + if (!settings.requireAuthorization && req.session && req.session.user) return cb(true); + } + + if (req.session && req.session.user && req.session.user.is_admin) return cb(true); + + // hooks.aCallFirst("authorize", {resource: req.path, req: req}, cb); + cb(false); } - - // Just pass if password is an empty string - if (pass === '') { - return next(); + + var authenticate = function (cb) { + // If auth headers are present use them to authenticate... + if (req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { + var userpass = new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString().split(":") + var username = userpass[0]; + var password = userpass[1]; + + if (settings.users[username] != undefined && settings.users[username].password == password) { + settings.users[username].username = username; + req.session.user = settings.users[username]; + return cb(true); + } + // return hooks.aCallFirst("authenticate", {req: req, username: username, password: password}, cb); + } + // hooks.aCallFirst("authenticate", {req: req}, cb); + cb(false); } - - - // If a password has been set and auth headers are present... - if (pass && req.headers.authorization && req.headers.authorization.search('Basic ') === 0) { - // ...check login and password - if (new Buffer(req.headers.authorization.split(' ')[1], 'base64').toString() === pass) { - return next(); + + + var failure = function () { + /* Authentication OR authorization failed. Return Auth required + * Headers, delayed for 1 second, if authentication failed. */ + res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); + if (req.headers.authorization) { + setTimeout(function () { + res.send('Authentication required', 401); + }, 1000); + } else { + res.send('Authentication required', 401); } } - // Do not require auth for static paths...this could be a bit brittle - else if (req.path.match(/^\/(static|javascripts|pluginfw)/)) { - return next(); - } - // Otherwise return Auth required Headers, delayed for 1 second, if auth failed. - res.header('WWW-Authenticate', 'Basic realm="Protected Area"'); - if (req.headers.authorization) { - setTimeout(function () { - res.send('Authentication required', 401); - }, 1000); - } else { - res.send('Authentication required', 401); - } + /* This is the actual authentication/authorization hoop. It is done in four steps: + + 1) Try to just access the thing + 2) If not allowed using whatever creds are in the current session already, try to authenticate + 3) If authentication using already supplied credentials succeeds, try to access the thing again + 4) If all els fails, give the user a 401 to request new credentials + + Note that the process could stop already in step 3 with a redirect to login page. + + */ + + authorize(function (ok) { + if (ok) return next(); + authenticate(function (ok) { + if (!ok) return failure(); + authorize(function (ok) { + if (ok) return next(); + failure(); + }); + }); + }); } exports.expressConfigure = function (hook_name, args, cb) { - args.app.use(exports.basicAuth); - // If the log level specified in the config file is WARN or ERROR the application server never starts listening to requests as reported in issue #158. // Not installing the log4js connect logger when the log level has a higher severity than INFO since it would not log at that level anyway. if (!(settings.loglevel === "WARN" || settings.loglevel == "ERROR")) args.app.use(log4js.connectLogger(httpLogger, { level: log4js.levels.INFO, format: ':status, :method :url'})); args.app.use(express.cookieParser()); + + /* Do not let express create the session, so that we can retain a + * reference to it for socket.io to use. Also, set the key (cookie + * name) to a javascript identifier compatible string. Makes code + * handling it cleaner :) */ + + args.app.sessionStore = new express.session.MemoryStore(); + args.app.use(express.session({store: args.app.sessionStore, + key: 'express_sid', + secret: apikey = randomString(32)})); + + args.app.use(exports.basicAuth); } diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 12fcc55c5..cb6a64033 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -80,15 +80,12 @@ exports.abiword = null; */ exports.loglevel = "INFO"; -/** - * Http basic auth, with "user:password" format - */ -exports.httpAuth = null; - -/** - * Http basic auth, with "user:password" format - */ -exports.adminHttpAuth = null; +/* This setting is used if you need authentication and/or + * authorization. Note: /admin always requires authentication, and + * either authorization by a module, or a user with is_admin set */ +exports.requireAuthentication = false; +exports.requireAuthorization = false; +exports.users = {}; //checks if abiword is avaiable exports.abiwordAvailable = function() diff --git a/src/package.json b/src/package.json index 83441da08..eda385b22 100644 --- a/src/package.json +++ b/src/package.json @@ -17,6 +17,7 @@ "ueberDB" : "0.1.7", "async" : "0.1.18", "express" : "2.5.8", + "connect" : "1.8.7", "clean-css" : "0.3.2", "uglify-js" : "1.2.5", "formidable" : "1.0.9",