UNPKG

keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

249 lines (234 loc) 8.19 kB
var crypto = require('crypto'); var keystone = require('../'); var scmp = require('scmp'); var utils = require('keystone-utils'); var _ = require('lodash'); /** * Creates a hash of str with Keystone's cookie secret. * Only hashes the first half of the string. */ function hash (str) { // force type str = '' + str; // get the first half str = str.substr(0, Math.round(str.length / 2)); // hash using sha256 return crypto .createHmac('sha256', keystone.get('cookie secret')) .update(str) .digest('base64') .replace(/\=+$/, ''); } /** * Signs in a user using user obejct * * @param {Object} user - user object * @param {Object} req - express request object * @param {Object} res - express response object * @param {function()} onSuccess callback, is passed the User instance */ function signinWithUser (user, req, res, onSuccess) { if (arguments.length < 4) { throw new Error('keystone.session.signinWithUser requires user, req and res objects, and an onSuccess callback.'); } if (typeof user !== 'object') { throw new Error('keystone.session.signinWithUser requires user to be an object.'); } if (typeof req !== 'object') { throw new Error('keystone.session.signinWithUser requires req to be an object.'); } if (typeof res !== 'object') { throw new Error('keystone.session.signinWithUser requires res to be an object.'); } if (typeof onSuccess !== 'function') { throw new Error('keystone.session.signinWithUser requires onSuccess to be a function.'); } req.session.regenerate(function () { req.user = user; req.session.userId = user.id; // if the user has a password set, store a persistent cookie to resume sessions if (keystone.get('cookie signin') && user.password) { var userToken = user.id + ':' + hash(user.password); var cookieOpts = _.defaults({}, keystone.get('cookie signin options'), { signed: true, httpOnly: true, maxAge: 10 * 24 * 60 * 60 * 1000, }); if (req.session.cookie) { req.session.cookie.maxAge = cookieOpts.maxAge; } res.cookie('keystone.uid', userToken, cookieOpts); } onSuccess(user); }); } exports.signinWithUser = signinWithUser; var postHookedSigninWithUser = function (user, req, res, onSuccess, onFail) { keystone.callHook(user, 'post:signin', req, function (err) { if (err) { return onFail(err); } exports.signinWithUser(user, req, res, onSuccess, onFail); }); }; /** * Signs in a user user matching the lookup filters * * @param {Object} lookup - must contain email and password * @param {Object} req - express request object * @param {Object} res - express response object * @param {function()} onSuccess callback, is passed the User instance * @param {function()} onFail callback */ var doSignin = function (lookup, req, res, onSuccess, onFail) { if (!lookup) { return onFail(new Error('session.signin requires a User ID or Object as the first argument')); } var User = keystone.list(keystone.get('user model')); if (typeof lookup.email === 'string' && typeof lookup.password === 'string') { // ensure that it is an email, we don't want people being able to sign in by just using "\." and a haphazardly correct password. if (!utils.isEmail(lookup.email)) { return onFail(new Error('Incorrect email or password')); } // create regex for email lookup with special characters escaped var emailRegExp = new RegExp('^' + utils.escapeRegExp(lookup.email) + '$', 'i'); // match email address and password User.model.findOne({ email: emailRegExp }).exec(function (err, user) { if (user) { user._.password.compare(lookup.password, function (err, isMatch) { if (!err && isMatch) { postHookedSigninWithUser(user, req, res, onSuccess, onFail); } else { onFail(err || new Error('Incorrect email or password')); } }); } else { onFail(err); } }); } else { lookup = '' + lookup; // match the userId, with optional password check var userId = (lookup.indexOf(':') > 0) ? lookup.substr(0, lookup.indexOf(':')) : lookup; var passwordCheck = (lookup.indexOf(':') > 0) ? lookup.substr(lookup.indexOf(':') + 1) : false; User.model.findById(userId).exec(function (err, user) { if (user && (!passwordCheck || scmp(passwordCheck, hash(user.password)))) { postHookedSigninWithUser(user, req, res, onSuccess, onFail); } else { onFail(err || new Error('Incorrect user or password')); } }); } }; exports.signin = function (lookup, req, res, onSuccess, onFail) { keystone.callHook({}, 'pre:signin', req, function (err) { if (err) { return onFail(err); } doSignin(lookup, req, res, onSuccess, onFail); }); }; /** * Signs the current user out and resets the session * * @param {Object} req - express request object * @param {Object} res - express response object * @param {function()} next callback */ exports.signout = function (req, res, next) { keystone.callHook(req.user, 'pre:signout', function (err) { if (err) { console.log("An error occurred in signout 'pre' middleware", err); } var cookieOpts = _.defaults({}, keystone.get('cookie signin options'), { signed: true, httpOnly: true, }); // Force the cookie to expire! cookieOpts.maxAge = 0; res.clearCookie('keystone.uid', cookieOpts); req.user = null; req.session.regenerate(function (err) { if (err) { return next(err); } keystone.callHook({}, 'post:signout', function (err) { if (err) { console.log("An error occurred in signout 'post' middleware", err); } next(); }); }); }); }; /** * Middleware to ensure session persistence across server restarts * * Looks for a userId cookie, and if present, and there is no user signed in, * automatically signs the user in. * * @param {Object} req - express request object * @param {Object} res - express response object * @param {function()} next callback */ exports.persist = function (req, res, next) { var User = keystone.list(keystone.get('user model')); if (!req.session) { console.error('\nKeystoneJS Runtime Error:\n\napp must have session middleware installed. Try adding "express-session" to your express instance.\n'); process.exit(1); } if (keystone.get('cookie signin') && !req.session.userId && req.signedCookies['keystone.uid'] && req.signedCookies['keystone.uid'].indexOf(':') > 0) { exports.signin(req.signedCookies['keystone.uid'], req, res, function () { next(); }, function (err) { var cookieOpts = _.defaults({}, keystone.get('cookie signin options'), { signed: true, httpOnly: true, }); // Force the cookie to expire! cookieOpts.maxAge = 0; res.clearCookie('keystone.uid', cookieOpts); req.user = null; next(); }); } else if (req.session.userId) { User.model.findById(req.session.userId).exec(function (err, user) { if (err) return next(err); req.user = user; next(); }); } else { next(); } }; /** * Middleware to enable access to Keystone * * Bounces the user to the signin screen if they are not signed in or do not have permission. * * req.user is the user returned by the database. It's type is Keystone.List. * * req.user.canAccessKeystone denotes whether the user has access to the admin panel. * If you're having issues double check your user model. Setting `canAccessKeystone` to true in * the database will not be reflected here if it is virtual. * See http://mongoosejs.com/docs/guide.html#virtuals * * @param {Object} req - express request object * @param req.user - The user object Keystone.List * @param req.user.canAccessKeystone {Boolean|Function} * @param {Object} res - express response object * @param {function()} next callback */ exports.keystoneAuth = function (req, res, next) { if (!req.user || !req.user.canAccessKeystone) { if (req.headers.accept === 'application/json') { return req.user ? res.status(403).json({ error: 'not authorised' }) : res.status(401).json({ error: 'not signed in' }); } var regex = new RegExp('^\/' + keystone.get('admin path') + '\/?$', 'i'); var from = regex.test(req.originalUrl) ? '' : '?from=' + req.originalUrl; return res.redirect(keystone.get('signin url') + from); } next(); };