Issue
I am building a web application using angular-fullstack. The stack is using express-sessions for session storage (in Mongodb) and passport.js for authentication.
I want to limit each user to a single login session. I am trying find a way to check if a user already has a living session when they login.
Is there a way to programmatically call a route to query mongodb from the passport middleware?
'use strict';
import path from 'path';
import passport from 'passport';
import {Strategy as LocalStrategy} from 'passport-local';
import express from 'express';
import session from 'express-session';
import _ from 'lodash';
import Session from '../../api/session/session.model';
var app = express();
require('run-middleware')(app);
function localAuthenticate(User, email, password, done, req) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
// HERE is where I am trying to check if a user
// already has a living session when they login
// I tried to use the runMiddleware
// to query mongodb for all the existing sessions
// but I get this error: http://pastebin.com/YTeu5AwA
app.runMiddleware('/sessions',{},function(code,data){
console.log(code) // 200
console.log(data) // { user: '20', name: 'Moyshale' }
});
// Is there a way to access and use an existing route?
user.authenticate(password, function(authError, authenticated) {
if (authError) {
return done(authError);
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
export function setup(User, config) {
passport.use(new LocalStrategy({
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
}, function(req, email, password, done) {
return localAuthenticate(User, email, password, done, req);
}));
}
Solution
Ok, I figured it out and I’ll try and explain what I did. My specific implementation required me to set up user ‘seats’, where each user is part of a group and each group is limited in N number of logins at a single time.
As I mentioned in the question, I am using the angular fullstack yeoman generator, so this solution is specific to that setup.
-
I created a ‘sessions’ API endpoint so that I could query and modify the sessions stored in the mongo db. I included a ‘seat’ record with type Number into the sessions model. This is used to keep track of the users seat status for each session. Each user is given a ‘loginSeat’ value which is used to populate this filed. Also the session now has a seatAllowed of type Boolean, true: the user is allowed to access the site, false: the user is not allowed access to the site.
'use strict'; import mongoose from 'mongoose'; var SessionSchema = new mongoose.Schema({ _id: String, session: String, expires: Date, seat: Number, seatAllowed: Boolean // true: the user is allowed to access the site, false: the user is not allowed access to the site }); export default mongoose.model('Session', SessionSchema);
-
I modified
server/auth/login/passport.js
so that when a user logs into the site, all other users with a matching seat are bumped out.'use strict'; import path from 'path'; import passport from 'passport'; import {Strategy as LocalStrategy} from 'passport-local'; import _ from 'lodash'; import Sessions from '../../api/session/session.model'; function saveUpdates(updates) { return function(entity) { var updated = _.merge(entity, updates); return updated.save() .then(updated => { return updated; }); }; } function localAuthenticate(User, email, password, done, req) { User.findOne({ email: email.toLowerCase() }).exec() .then(user => { if (!user) { return done(null, false, { message: 'This email is not registered.' }); } // When a user logs into the site we flag their seat as allowed var updateSession = {'seat': user.loginSeat, 'seatAllowed': true}; Sessions.findById(req.session.id).exec() .then(saveUpdates(updateSession)) // When a user logs into the site, we disallow the seats of all other sessions with matching seat Sessions.find().exec() .then(sessions => { // Check for existing user logged in with matching login seat for (var i = 0; i < sessions.length; i++) { if (sessions[i].seat === user.loginSeat && sessions[i].id !== req.session.id) { console.log('DISALOW SEAT:'); var updateSession = {'seatAllowed': false}; Sessions.findById(sessions[i].id).exec() .then(saveUpdates(updateSession)); } } }); user.authenticate(password, function(authError, authenticated) { if (authError) { return done(authError); } if (!authenticated) { return done(null, false, { message: 'This password is not correct.' }); } else { return done(null, user); } }); }) .catch(err => done(err)); } export function setup(User, config) { passport.use(new LocalStrategy({ passReqToCallback: true, usernameField: 'email', passwordField: 'password' // this is the virtual field on the model }, function(req, email, password, done) { return localAuthenticate(User, email, password, done, req); })); }
-
Each time the client makes a request the
isAuthenticated
function is triggered. This is where I check for the seaAllowed boolean for the current session, if true, allow the user to access the site, otherwise logout the user:function saveUpdates(updates) { return function(entity) { var updated = _.merge(entity, updates); return updated.save() .then(updated => { return updated; }); }; } /** * Attaches the user object to the request if authenticated * Otherwise returns 403 */ export function isAuthenticated() { return compose() // Validate jwt .use(function(req, res, next) { // Allow access_token to be passed through query parameter as well if (req.query && req.query.hasOwnProperty('access_token')) { req.headers.authorization = 'Bearer ' + req.query.access_token; } validateJwt(req, res, next); }) // Attach user to request .use(function(req, res, next) { User.findById(req.user._id).exec() .then(user => { if (!user) { return res.status(401).end(); } req.user = user; /////////////////////////// // Login seat limitation // /////////////////////////// // Check if the user seat is allowed Sessions.findById(req.session.id).exec() .then(thisSession => { // TODO access the session in a better way if (thisSession.seatAllowed === false || thisSession.seatAllowed === undefined) { res.redirect('/login'); } }) next(); }) .catch(err => next(err)); }); }
Thats it.
Answered By – Bwyss
Answer Checked By – Marie Seifert (AngularFixing Admin)