Search code examples
javascriptexpresspassport.jsnext.jspassport-local

Passport.js in Next.js app not saving user across routes or app


I am using passport.js specifically the local-strategy for authentication with my next.js application.

Making requests to my data store is fine, and auth, authorization works as well. But I'd like access to the req.user for another route so I can have access to that users._id.

As the passport docs indicate:

By default, if authentication fails, Passport will respond with a 401 Unauthorized status, and any additional route handlers will not be invoked. If authentication succeeds, the next handler will be invoked and the req.user property will be set to the authenticated user.

This is my Next.js app's server.js file:

var express = require('express');

require('dotenv').config();
var morgan = require('morgan');
var cookieParser = require('cookie-parser');
var cors = require('cors');

var nextJS = require('next');
var session = require('express-session');
var mongoose = require('mongoose');
var MongoStore = require('connect-mongo')(session);
var path = require('path');
var bodyParser = require('body-parser');
var auth = require('./lib/auth');
var HttpStatus = require('http-status-codes');
var compression = require('compression');
var helmet = require('helmet');

var PORT = process.env.PORT || 8016;

var { isBlockedPage, isInternalUrl } = require('next-server/dist/server/utils');

function NODE_ENVSetter(ENV) {
  var environment,
    environments = {
      production: () => {
        environment = process.env.MONGODB_URI;
        console.log(`We are currently in the production environment: ${environment}`);
        return environment;
      },
      test: () => {
        environment = process.env.TEST_DB_DSN;
        console.log(`We are currently in the test environment: ${environment}`);
        return environment;
      },
      default: () => {
        environment = process.env.DEVELOPMENT_DB_DSN;
        console.log(`We are currently in the development environment: ${environment}`);
        return environment;
      }
    };
  (environments[ENV] || environments['default'])();

  return environment;
}

var db = NODE_ENVSetter('development');

function start() {
  const dev = process.env.NODE_ENV !== 'production';
  const app = nextJS({ dev });
  const server = express();
  // const proxy = createProxyMiddleware(options);

  app
    .prepare()
    .then(() => {
      mongoose.connect(db, {
        useNewUrlParser: true,
        useUnifiedTopology: true
      });
      mongoose.Promise = global.Promise;

      mongoose.connection
        .on('connected', () => {
          console.log(`Mongoose connection open on ${db}`);
        })
        .on('error', err => {
          console.log(`Connection error: ${err.message}`);
        });
    })
    .catch(err => {
      console.error(err);
    });

  server.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.header(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept'
    );
    res.header('Access-Control-Allow-Credentials', true);
    res.header('Access-Control-Allow-Methods', '*'); // enables all the methods to take place
    return next();
  });

  server.use(function(req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Credentials', true);
    res.header(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept'
    );
    next();
  });

  server.use(morgan('dev'));
  server.set('view engine', 'html');
  server.use(express.static(path.join(__dirname + 'uploads')));
  server.use('/uploads', express.static('uploads'));

  server.use(cors());
  server.use(cookieParser());
  server.use(bodyParser.json());
  server.use(
    session({
      secret: 'very secret 12345',
      resave: true,
      saveUninitialized: false,
      store: new MongoStore({ mongooseConnection: mongoose.connection })
    })
  );
  server.use(bodyParser.urlencoded({ limit: '50mb', extended: false }));

  server.use(auth.initialize);
  server.use(auth.session);

  server.use(compression());
  server.use(helmet());

  server.use('/users', require('./users'));

Here I've included a log to check the req.user object

  server.use((req, res, next) => {
    console.log('req.user', req.user);
    next();
  });

  // Redirect all requests to main entrypoint pages/index.js
  server.get('/*', async (req, res, next) => {
    try {
      // @NOTE code duplication from here
      // https://github.com/zeit/next.js/blob/cc6fe5fdf92c9c618a739128fbd5192a6d397afa/packages/next-server/server/next-server.ts#L405
      const pathName = req.originalUrl;
      if (isInternalUrl(req.url)) {
        return app.handleRequest(req, res, req.originalUrl);
      }

      if (isBlockedPage(pathName)) {
        return app.render404(req, res, req.originalUrl);
      }

      // Provide react-router static router with a context object
      // https://reacttraining.com/react-router/web/guides/server-rendering
      req.locals = {};
      req.locals.context = {};
      const html = await app.renderToHTML(req, res, '/', {});

      // Handle client redirects
      const context = req.locals.context;
      if (context.url) {
        return res.redirect(context.url);
      }

      // Handle client response statuses
      if (context.status) {
        return res.status(context.status).send();
      }

      // Request was ended by the user
      if (html === null) {
        return;
      }

      app.sendHTML(req, res, html);
    } catch (e) {
      next(e);
    }
  });

  // eslint-disable-next-line func-names

  class AppError extends Error {
    constructor(message, statusCode) {
      super(message);

      this.statusCode = statusCode;
      this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
      this.isOperational = true;

      Error.captureStackTrace(this, this.constructor);
    }
  }

  server.all('*', (req, res, next) => {
    next(new AppError(`Can't find ${req.originalUrl} on this server!`, 404));
  });

  if (process.env.NODE_ENV === 'production') {
    server.use(express.static('.next/static'));

    server.get('*', (req, res) => {
      res.sendFile(path.resolve(__dirname, '.next/static', 'index.html'));
    });

    server.listen(PORT, err => {
      if (err) throw err;
      console.log(
        `> Ready and listening on PORT:${PORT} in the ${process.env.NODE_ENV} environment`
      );
    });
  } else {
    server.listen(PORT, err => {
      if (err) throw err;
      console.log(`> Ready and listening on http://localhost:${PORT}`);
    });
  }
}

start();

And this is my auth file:

var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var UserModel = require('../models/UserModel');

passport.use(
  new LocalStrategy(
    { usernameField: 'username', passwordField: 'password' },
    async (username, password, done) => {
      try {
        const user = await UserModel.findOne({ username: username }).exec();
        if (!user) {
          return done(null, false, { message: 'Invalid username!' });
        }
        const passwordOk = await user.comparePassword(password);

        if (!passwordOk) {
          return done(null, false, {
            message: 'Invalid password!'
          });
        }
        return done(null, user);
      } catch (err) {
        return done(err);
      }
    }
  )
);

// eslint-disable-next-line no-underscore-dangle
passport.serializeUser((user, done) => done(null, user._id));

passport.deserializeUser(async (id, done) => {
  try {
    const user = await UserModel.findById(id).exec(); //exec is used to get a real Promise

    console.log('user deserialUser', user);
    return done(null, user);
  } catch (err) {
    return done(err);
  }
});

module.exports = {
  initialize: passport.initialize(),
  session: passport.session()
};

And it my /login route:

router.route('/login').post((req, res, next) => {
  passport.authenticate('local', (err, user) => {
    console.log('user 60', req.user);
    console.log('user ', user);

    // console.log('res.locals.user ', res.locals.user);
    if (!user) {
      return res.status(404).send({
        msg: [
          `We were unable to find this user.`,
          `This email and/or password combo may be incorrect.
          Please confirm with the "Forgot password" link above or the "Register" link below!`
        ]
      });
    }

    if (user.isVerified === false) {
      return res.status(401).send({
        msg: [
          'Your username has not been verified!',
          'Check your email for a confirmation link.'
        ]
      });
    } else {

I made a log to see if user would be on the req object

      console.log('req.user in success login route', req.user);
      return res.status(200).send({
        msg: [`Your have successfully logged in;`, `Welcome to Hillfinders!`]
      });
    }
  })(req, res, next);
});

Solution

  • It appears what fixed my problem was the way I was declaring my routes.

    This works:

    router.post('/login', passport.authenticate('local'), function(req, res) {
      var user = req.user;
    
      if (!user) {
        return res.status(404).send({
          msg: [
            `We were unable to find this user.`,
            `This email and/or password combo may be incorrect.
              Please confirm with the "Forgot password" link above or the "Register" link below!`
          ]
        });
      }
    
      if (user.isVerified === false) {
        return res.status(401).send({
          msg: [
            'Your username has not been verified!',
            'Check your email for a confirmation link.'
          ]
        });
      } else {
        return res.status(200).send({
          msg: [`Your have successfully logged in;`, `Welcome to Hillfinders!`]
        });
      }
    

    });

    I was declaring it like this:

    router.route('/login').post((req, res, next) => {
      passport.authenticate('local', (err, user) => {
        console.log('user ', user);
    
        // console.log('res.locals.user ', res.locals.user);
        if (!user) {
          res.status(404).send({
            msg: [
              `We were unable to find this user.`,
              `This email and/or password combo may be incorrect.
              Please confirm with the "Forgot password" link above or the "Register" link below!`
            ]
          });
          return;
        }
    
        if (user.isVerified === false) {
          res.status(401).send({
            msg: [
              'Your username has not been verified!',
              'Check your email for a confirmation link.'
            ]
          });
          return;
        } else {
          res.status(200).send({
            msg: [`Your have successfully logged in;`, `Welcome to Hillfinder!`]
          });
          return;
        }
      })(req, res, next);
    });
    

    Think I saw this syntax in the Express docs and just thought it was cool because of the chaining:

     router.route('/login').post((req, res, next)=>{})
    

    There must be some reason to declare it like this instead of the normal way...

    Anyway thats' what eventually got me the user from passport to be on the req object \o/, This might also help other libraries that add objects of value to the req object too...