Passport middleware, check if the user already has a living session from

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.

enter image description here

As I mentioned in the question, I am using the angular fullstack yeoman generator, so this solution is specific to that setup.

  1. 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); 
    
  2. 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);
      }));
    }
    
  3. 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)

Leave a Reply

Your email address will not be published.