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 to.
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
Let's try running the app to make sure it works.
$ npm install
$ export SESSION_SECRET=some_secret_value_here
$ npm run dev
Open http://localhost:3000 in your browser, and you should see something like this.
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));
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');
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'
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
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
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
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
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 require
s.
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;
}
});
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]);
});
});
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);
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)
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) });
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;
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;
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".
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
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.
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.