Node.js Session Management Using Express Sessions, Redis, and Passport - Part 2

jankleinert

Jan Kleinert

Posted on July 19, 2019

Node.js Session Management Using Express Sessions, Redis, and Passport - Part 2

In Part 1 of this tutorial, we went step-by-step through the process of building a web app with Node.js and Express that uses express-session and connect-redis as a way of helping users understand how session management works.

In this second part, we will expand on the previous app by implementing authentication using Passport and exploring how authentication and sessions work together.

Pre-requisites

If you followed the steps in Part 1, then you can move on to the next section. If not, here's what you need to do.

Clone this GitHub repo that has the code for the demo app. The master branch contains the code as it is at the end of Part 1. You'll also need to install Redis and start the Redis server if you don't already have it installed. If you need to install Redis, you can take a look at this documentation.

$ git clone https://github.com/jankleinert/redis-session-demo
$ cd redis-session-demo
Enter fullscreen mode Exit fullscreen mode

Let's try running the app to make sure it works.

$ npm install
$ export SESSION_SECRET=some_secret_value_here
$ npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 in your browser, and you should see something like this.

demo app screenshot

Set up a MySQL user database

Regardless of whether or not you completed Part 1, you'll need to ensure you have MySQL installed. Instructions are here if you need to install and set up MySQL. Next, launch mysql and create a new database and a new table.

mysql> CREATE DATABASE redis_session_demo; 
mysql> USE redis_session_demo;
mysql> CREATE TABLE users (id varchar(20), email varchar(20), password varchar(60));
Enter fullscreen mode Exit fullscreen mode

This will be our user database. To speed things up, rather than having an account creation page, we'll manually insert a test user into the database. The app will be using bcrypt to create a hash for our passwords. Our test user will have id = a1b2c3d4, email = test@example.com, and password = password. You can use this site to create a hashed password. Next, we'll insert that into our user database.

mysql> INSERT INTO users (id, email, password) VALUES ('a1b2c3e4', 'test@example.com', '$2y$12$7Mj1fG3bdlpmRcXtZpwimOI4pItCQcj5x2.ZqydPbR5wWlKGVaQVe');               
Enter fullscreen mode Exit fullscreen mode

Quick recap of the demo app

The demo app was built using express-generator to create the app skeleton. It's using Pug for the view engine. When you click the Pour Another button, it makes a request to an API that will return a machine-learning-generated craft beer name.

In Part 1, we added a session information panel that displays the session ID, how many more seconds are left before the session expires, and also our session data: the number of beer names that have been viewed in the current session. To implement session management, we used express-session for the session middleware and connect-redis as the session store. In the next step, we will add links to log in and log out, create a login page, and refactor the session panel that was originally included directly in /views/index.pug.

Add authentication support to the frontend

We are going to start by refactoring the session info panel. By moving it to a separate file, it will be easier to include it in multiple pages. Create a new file /views/session.pug and paste in this code. There is a section at the bottom now that displays whether or not the user is authenticated.

    .session
      p Session Info
      if sessionID
        p= 'Session ID: ' + sessionID 
      if sessionExpireTime
        p= 'Session expires in ' + Math.round(sessionExpireTime/1000) + ' seconds'
      if beersViewed
        p= 'Beers viewed in this session: ' + beersViewed
      else 
        p= 'No beers viewed yet in this session.'
      if isAuthenticated
        p= 'Logged in as: ' + email
      else  
        p= 'Not logged in'                      
Enter fullscreen mode Exit fullscreen mode

Now, open up /views/index.pug and replace the .session section with the following line. It should be lined up in the same column as the h1.

    include session.pug              
Enter fullscreen mode Exit fullscreen mode

It's time to create the login page. Create /views/login.pug and paste in this code. It's a simple form with fields for email and password.

    extends layout

    block content
      h1= 'Log In'
      .lead
        form#login-form(action='/login', method='post')
          .form-group
            input(name='email', type='text', placeholder='Email', required='')
          .form-group
            input(name='password', type='password', placeholder='Password', required='')
          button.btn.btn-primary(type='submit')= 'Log In'
        if error
          p= error

      include session.pug              
Enter fullscreen mode Exit fullscreen mode

Now, we need to add Home, Log In, and Log Out links to the navigation. Open layout.pug and add this code directly below h3.masthead-brand Craft Beer Name Demo.

    nav.nav.nav-masthead.justify-content-center
      a.nav-link(href='/') Home
      a.nav-link(href='/login') Log In
      a.nav-link(href='/logout') Log Out
Enter fullscreen mode Exit fullscreen mode

Update app.js

To support adding authentication to the app, we need to install some additional packages.

npm install --save bcryptjs mysql passport passport-local                       
Enter fullscreen mode Exit fullscreen mode

bcryptjs is used for for hashing and checking passwords. passport is the authentication middleware we are using, and passport-local is the authentication strategy, meaning we are authenticating with a username and password.

Next, open up app.js and add the following code below the existing requires.

const loginRouter = require('./routes/login');
const logoutRouter = require('./routes/logout');
const mysql = require('mysql');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');

const mysqlConnection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '', // demo purposes only
  database: 'redis_session_demo'
});

mysqlConnection.connect(function(err) {
  if (err) {
    console.log('error connecting to mysql: ' + err.stack);
    return;
  }
});
Enter fullscreen mode Exit fullscreen mode

Note that we're using a MySQL database with root and '' as the login and password. This is only for demo purposes; don't do that in production! You probably also noticed loginRouter and logoutRouter reference files that don't exist. We'll create those in the next section.

Scroll down a bit until you see const redisClient = redis.createClient();. Directly after that line, add the following code.

// configure passport.js to use the local strategy
passport.use(new LocalStrategy(
  { usernameField: 'email' },
  (email, password, done) => {
    mysqlConnection.query('SELECT * FROM users WHERE email = ?', [email], function (error, results, fields) {
      if (error) throw error;
      var user = results[0];
      if (!user) {
        return done(null, false, { message: 'Invalid credentials.\n' });
      }
      if (!bcrypt.compareSync(password, user.password)) {
        return done(null, false, { message: 'Invalid credentials.\n' });
      }
      return done(null, user);
    });
  }
));

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  mysqlConnection.query('SELECT * FROM users WHERE id = ?', [id], function (error, results, fields) {
    if (error) {
      done(error, false);
    }
    done(null, results[0]);  
  });
});              
Enter fullscreen mode Exit fullscreen mode

I found this article very helpful in understanding what happens during the authentication process. You'll notice that I modeled some parts of this code after what was done in that article.

Scroll down a bit more in the file until you find app.use('/', indexRouter);. Replace that line with the following code.

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

app.use('/', indexRouter);
app.use('/login', loginRouter);
app.use('/logout', logoutRouter);                       
Enter fullscreen mode Exit fullscreen mode

This code is setting up our app to use passport as middleware, and then we're adding the two new routes for logging in and logging out.

Update routes

The last step we need to take is to update /routes/index.js and create two new files: /routes/login.js and /routes/logout.js. Open /routes/index.js. In each of the four res.render() calls, add this to the end of the list of properties in the locals object.

, isAuthenticated: req.isAuthenticated(), email: (req.isAuthenticated() ? req.user.email : null)              
Enter fullscreen mode Exit fullscreen mode

So, for example, the res.render() call in router.get() would become:

res.render('index', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, beerName: null, beerStyle: null, error: null, isAuthenticated: req.isAuthenticated(), email: (req.isAuthenticated() ? req.user.email : null) });
Enter fullscreen mode Exit fullscreen mode

Next create /routes/login.js and paste in this code.

const express = require('express');
const router = express.Router();
const passport = require('passport');

/* GET request for login page */
router.get('/', function(req, res, next) {
  var expireTime = new Date(req.session.cookie.expires) - new Date();
  res.render('login', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, error: null, isAuthenticated: req.isAuthenticated(), email: (req.isAuthenticated() ? req.user.email : null) });
});


router.post('/', function (req, res, next) {
  var expireTime = new Date(req.session.cookie.expires) - new Date();
  passport.authenticate('local', (err, user, info) => {
    if(info) {return res.send(info.message)}
    if (err) { return next(err); }
    if (!user) { return res.redirect('/login'); }
    req.login(user, (err) => {
      if (err) { return next(err); }
      res.render('login', {sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, username: req.user.id, error: null, isAuthenticated: req.isAuthenticated(), email: (req.isAuthenticated() ? req.user.email : null)});
    })
  })(req, res, next);
})  

router.get('')

module.exports = router;              
Enter fullscreen mode Exit fullscreen mode

router.post() is where our login form submissions are handled using passport, using the local strategy.

Finally create /routes/logout.js and paste in this code.

const express = require('express');
const router = express.Router();

/* GET request for logout page */
router.get('/', function(req, res, next) {
  var expireTime = new Date(req.session.cookie.expires) - new Date();   
  req.logout();
  req.session.destroy(function() {
    res.redirect('/');
  });
});

router.get('')

module.exports = router;              
Enter fullscreen mode Exit fullscreen mode

There is no page that is displayed specifically for logging out. Instead, when the GET request is made to /logout, the app will log the user out, destroy the session, and then redirect them to the home page. At this point there will not be an authenticated user, and a new session will be created.

Try it!

Let's try it out! Open http://localhost:3000 in your browser. When it first loads, you should see the info panel displays a session ID and a time until the session expires, as well as "Not logged in".

add auth

Click the "Log In" link in the header and authenticate using our test credentials: test@example.com / password. When the page reloads, you should see that you are now logged in as test@example.com

user logged in

As you take other actions on the site, you'll see that you stay logged in, but if you click "Log Out" in the navigation, you will no longer be logged in and a new session will be started.

user logged out

That's it! You now have a simple app that handles session management as well as authentication. Is it complete? Definitely not! There are lots of improvements and additions that could be made, including an account creation page, better error handling, etc. You can find the complete code for Part 2 on GitHub.

💖 💪 🙅 🚩
jankleinert
Jan Kleinert

Posted on July 19, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related