@Version : 4.5.0
@Build : 94d077c24
By using this site, you acknowledge that you have read and understand the Cookie Policy, Privacy Policy, and the Terms. Close

NextJS PassportJS LocalStrategy Authentication

Posted Friday, April 3rd, 2020

NodeJSNextJS
NextJS PassportJS LocalStrategy Authentication

I recently did a tutorial on how to do a cookie based stateless authentication on a NextJS app using JWT and PassportJS using GitHub OAuth API. This one is based on a stateful authentication where session is stored on the server. Consider the diagram below.

So here we have a setup where when a user clicks the login button, we make a POST call to our API to validate the user credentials. In this process, Passportjs will create a session for the login if successful. The session id will be stored on the frontend as a cookie while session data is stored on the server side in memory or a storage store like Redis, MongoDB. The cookie will be sent back with each request that goes back and used to fetch the session data on the server side.

You should note that this works because the API and the UI are served from the same origin. If they are served from a different origin, its a whole different story, we have to manually set the session cookie to the chain between the browser and the UI server so that the UI server can send it to the API server upon each request requests. This will manual setup will also be responsible for availing the session cookie on the browser. Lets write some code!

To start with, create a new nextjs app using npx create-next-app your_app..

Install expressjs to serve our app and also server the auth API.

npm i --save express

Now create the express server that serves our SSR pages and also has the authentication endpoints. By default it will just mount Nextjs server.

// server.js

const express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {

  const server = express();

  server.get('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(3000, err => {
    if (err) throw err;
    console.info(`> Ready on http://localhost:${3000}`);
  });
});

Next we setup passport on the server. We will use passport local strategy based on username and password. We also use express-session to manage the session and axios to communicate from the browser to the API

npm i --save body-parser express-session axios uid-safe passport passport-local

To do this, we initialize passport in our server request chain and add utilities to serialize and deserialize the user. We also add session configuration.

// server.js

const express = require('express');
const next = require('next');
const passport = require('passport')
const session = require("express-session");
const uid = require('uid-safe');
const bodyParser = require("body-parser");

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {

  const server = express();

  const sessionConfig = {
    secret: uid.sync(18),
    cookie: {
      maxAge: 86400 * 1000
    },
    resave: false,
    saveUninitialized: true
  };

  server.use(bodyParser.json());

  server.use(session(sessionConfig));

  passport.serializeUser((user, done) => done(null, user));
  passport.deserializeUser((user, done) => done(null, user));

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

  server.get('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(3000, err => {
    if (err) throw err;
    console.info(`> Ready on http://localhost:${3000}`);
  });

});

Next we add a middleware to enforce auth on the root path. When passport is fully setup there is a function isAuthenticated for checking if user is authentiacted so we use that to redirect the user to login if they are not logged in.

// server.js
........

  server.get('/', (req, res, next) => {
    if(!req.isAuthenticated()) return res.redirect('/login')
    next();
  });

  server.get('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(3000, err => {
    if (err) throw err;
    console.info(`> Ready on http://localhost:${3000}`);
  });

});

Now we create the auth route. This includes the local strategy passport authentication and a logout endpoint. Note that inside the validateUser function below, we can call any API or DB to validate login credentials.

// api/auth.js

const express = require('express')
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy

const router = express()

const users = [{ name: "Test User", password: "password", user_name: "zemuldo" }]

const validateUser = async (username, password, done) => {
    const user = await users.find(u => u.user_name == username)
    if (!user) { return done(null, false); }
    if (!user.password === password) { return done(null, false); }
    return done(null, user);
}

passport.use(new LocalStrategy(validateUser));

router.post('/login', (req, res, next) => {
    passport.authenticate('local',
        (err, user) => {
            if (err) {
                return res.status(400).send({ error: err })
            }

            if (!user) {
                return res.status(400).send({ error: "Login failed" });
            }

            req.logIn(user, function (err) {
                if (err) {
                    return next(err);
                }

                return res.send({ status: "ok" });
            });

        })(req, res, next);
});

module.exports = router;

Then we mount the auth root on the server. At this point we have the login endpoints ready.

// server.js
......
		server.use('/auth', require('./api/auth'))

		server.get('/', (req, res, next) => {
			if(!req.isAuthenticated()) return res.redirect('/login')
			next();
		});

		server.get('*', (req, res) => {
			return handle(req, res);
		});

		server.listen(3000, err => {
			if (err) throw err;
			console.info(`> Ready on http://localhost:${3000}`);
		});

});

Now we have just create an extra page for login. Login page has a form for signing in.

// pages/login.js

import Head from 'next/head'
import axios from 'axios'

class Home extends React.Component {
    call_api = (e) => {
        e.preventDefault()
        axios.post('/auth/login', { username: "zemuldo", password: "password" })
            .then((data) => {
                console.log(data.data)
                window.location = '/'
            })
            .catch(err => console.log(err))
    }
    render() {
        return (<div>
            <form>
                <p>Login Here</p>
                <input />
                <input />
                <button onClick={this.call_api}>Login</button>
            </form>
        </div>)
    }
}

export default Home

And we modify home page to get the user when server side rendering or redirect to login if user is not found.

// pages/index.js

import Head from 'next/head'

const Home = () => (
  <div className="container">
   Home
  </div>
)

Home.getInitialProps = (ctx) => {
  let pageProps = {};
 
  if (ctx.req && ctx.req.session.passport) {
    pageProps.user = ctx.req.session.passport.user;
  }
  if(!pageProps.user) {
    ctx.res.writeHead(302, { Location: '/login' }).end()
  }
  return { pageProps };
}

export default Home

Now when you start the server, redirect to login should work and after login, the index page should render.

Where there is login, there has to be logout. To setup logout, we add a logout handler on the auth route and add a button to the index page.

import Head from 'next/head'
import Axios from 'axios';

const Home = () => (
  <div className="container">
   <p>Home</p>

   <button onClick={(e) => {
      e.preventDefault()
      Axios.post('/auth/logout').then(() => window.location = '/login')
    }}>
      Logout
     </button>

  </div>
)

Home.getInitialProps = (ctx) => {
  let pageProps = {};
 
  if (ctx.req && ctx.req.session.passport) {
    pageProps.user = ctx.req.session.passport.user;
  }
  if(!pageProps.user) {
    ctx.res.writeHead(302, { Location: '/login' }).end()
  }
  return { pageProps };
}

export default Home

Logout handler still users passport to logout the user and delete the session data.

// api/auth.js
.......
router.post('/logout', function(req, res){
    req.logout();
    res.send("ok");
  });

module.exports = router;

Using a persistent store - Redis.

So far the setup works but every server restart loses the session data forcing user to login again. To solve this, we configure passport to persist the session in a store like Redis or MongoDB or just anything that is supported or configurable. For this let us use Redis to persist the session data. Install redis on your system using the quick start.

First thing is to install connect-redis and redis then configure session middleware to use redis.

npm i --save redis connect-redis express-session

Configure your server as below, notice we now use static secret for our session secret because we want persisting session data.

// server.js

const express = require('express');
const next = require('next');
const passport = require('passport')
const session = require("express-session");
const uid = require('uid-safe');
const bodyParser = require("body-parser");

// require redis
const redis = require('redis')
 
 // create store and client
const RedisStore = require('connect-redis')(session)
const redisClient = redis.createClient()

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {

  const server = express();

 // add store to the session config and use a static secret.
  const sessionConfig = {
    store: new RedisStore({ client: redisClient }),
    secret: 'static session key that is persisting',
    cookie: {
      maxAge: 86400 * 1000
    },
    resave: false,
    saveUninitialized: true
  };

  server.use(bodyParser.json());

  server.use(session(sessionConfig));

  passport.serializeUser((user, done) => done(null, user));
  passport.deserializeUser((user, done) => done(null, user));

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

  server.use('/auth', require('./api/auth'))

  server.get('/', (req, res, next) => {
    if(!req.isAuthenticated()) return res.redirect('/login')
    next();
  });

  server.get('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(3000, err => {
    if (err) throw err;
    console.info(`> Ready on http://localhost:${3000}`);
  });

});

Now session should persist even after restarting your server and even after restarting your server. You can also see the session data in redis using redis CLI utility redis-cli.

redis-cli --scan --pattern 'sess:*'
#sess:01e21jWQ_nk9Z_bmqJleRASC7iO5fA-f
redis-cli GET sess:01e21jWQ_nk9Z_bmqJleRASC7iO5fA-f
"{\"cookie\":{\"originalMaxAge\":86400000,\"expires\":\"2020-04-05T10:31:26.265Z\",\"httpOnly\":true,\"path\":\"/\"},\"passport\":{\"user\":{\"name\":\"Test User\",\"password\":\"password\",\"user_name\":\"zemuldo\"}}}"

Try to delete the session data on redis and see what happens. The UI should redirect to login as the session data has not been deleted in redis store.

 redis-cli
127.0.0.1:6379> flushall
OK
127.0.0.1:6379>

The entire code used here is on GitHub.



Thank you for finding time to read my post. I hope you found this helpful and it was insightful to you. I enjoy creating content like this for knowledge sharing, my own mastery and reference.

If you want to contribute, you can do any or all of the following 😉. It will go along way! Thanks again and Cheers!