Recently, I set out to create a demo application for my talk at Redis Day NYC that illustrates how session management works in a Node.js/Express web app, using Redis as the session store and then adds authentication on top of all that. Understanding the concepts and how they work together is one thing, but I hadn't actually built an app that used all these components together before.
As part of my initial research, I looked for existing tutorials or examples that did what I was trying to do. I found several good blog posts and tutorials, but none did exactly what I was looking for. Part 1 of this tutorial will take you 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. Part 2 will expand on this by implementing authentication using Passport and exploring how authentication and sessions work together.
Get the code for the craft beer name demo app
We will be starting with a simple demo app, and once we have that up and running, we'll add in session management and then authentication. Let's start by cloning the GitHub repo that has the code for the demo app and then switch to the beer-demo branch.
$ git clone https://github.com/jankleinert/redis-session-demo
$ cd redis-session-demo
$ git checkout beer-demo
Let's try running the app to make sure it works.
$ npm install
$ npm run dev
Open http://localhost:3000 in your browser, and you should see something like this.
Understanding 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. That's really all the app does at this point.
The three main files we'll be working with are app.js
, /routes/index.js
, and /views/index.pug
.
Why do we care about session management anyway?
"Session" is such an overloaded term, and can mean very different things depending on context. In this tutorial, we're talking about a user’s session in a web application. You can think of it as the set of requests and responses within a web app, initiated by a single user, from the start of their interaction until they end the session or it expires.
Why do we care about or need a construct like a session? HTTP is stateless, so each request and response pair is independent of the others. By default, no state is maintained and the server doesn’t know who you are from one request to another. Session management gives us the ability to assign an identifier to a user session, and use that ID to store state or data relevant to the session. This could be something like whether or not a user is authenticated, the items in a shopping cart, and so on - whatever state needs to be kept during that session.
There are multiple ways of handling session management, but we’re going to look at one specific way, where session data is kept in a session store, and we’ll be using Redis as the session store.
On the client side, a cookie is stored with the session ID but none of the session data. In your application’s session store (Redis in this case), the session ID is also stored, along with the session data.
Add a session info panel to the app
To make it easy to visualize what's happening with a session, we will add a session info panel to the app. Open /views/index.pug
and add
the following code to the bottom of the file. Be careful with your indentation; .session
should line up in the same column as h1
.
.session
p Session Info
if sessionID
p= 'Session ID: ' + sessionID
if sessionExpireTime
p= 'Session expires in ' + Math.round(sessionExpireTime) + ' seconds'
if beersViewed
p= 'Beers viewed in this session: ' + beersViewed
This panel will display 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 this session. We'll be specifying those values in /routes/index.js
in a later step.
Add express-session and connect-redis to app.js
express-session
is session middleware for Express. It's pretty straightforward to set up and use. There are quite a
few compatible session stores that you can use for storing session data. We will be using connect-redis
.
Let's start by installing the npm modules that we need.
$ npm install --save express-session uuid redis connect-redis
Next, open up app.js
and add the following code below the existing require
s. uuid
will be used to generate a unique ID to use for our session ID.
const uuid = require('uuid/v4')
const session = require('express-session');
const redis = require('redis');
const redisStore = require('connect-redis')(session);
const redisClient = redis.createClient();
redisClient.on('error', (err) => {
console.log('Redis error: ', err);
});
Before we move on, make sure you have Redis installed and that the Redis server is running. If you need to install Redis, you can take a look at this documentation.
Now we can set up the session middleware and tell it to use our Redis store as the session store. Add this code above the line app.use('/', indexRouter);
.
app.use(session({
genid: (req) => {
return uuid()
},
store: new redisStore({ host: 'localhost', port: 6379, client: redisClient }),
name: '_redisDemo',
secret: process.env.SESSION_SECRET,
resave: false,
cookie: { secure: false, maxAge: 60000 }, // Set to secure:false and expire in 1 minute for demo purposes
saveUninitialized: true
}));
There are a couple things to note about this code. The cookie that stores the session ID will be named "_redisDemo". We are using
an environment variable to set the secret. In the next step, we'll export that env variable (you can set it to whatever you like). We are setting the session expiration to 1
minute to make it easier to understand what's happening in the demo app. In a real application, you'd set the maxAge to something more
reasonable for your application. In your terminal, stop nodemon
and then run the following.
$ export SESSION_SECRET=some_secret_value_here && npm run dev
Add session management code to /routes/index.js
The last step will be to add logic to keep track of the number of beer names viewed per session and to pass the session-related information through to the session panel.
Open /routes/index.js
and replace the existing get
and post
with the code below.
router.get('/', function(req, res, next) {
var expireTime = req.session.cookie.maxAge / 1000;
res.render('index', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, beerName: null, beerStyle: null, error: null });
});
router.post('/', function (req, res) {
request('https://www.craftbeernamegenerator.com/api/api.php?type=trained', function (err, response, body) {
if (req.session.views) {
req.session.views++
} else {
req.session.views = 1
}
var expireTime = req.session.cookie.maxAge / 1000;
if(err){
res.render('index', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, beerName: null, beerStyle: null, error: 'Error, please try again'});
} else {
var beerInfo = JSON.parse(body)
if(beerInfo.status != 200){
res.render('index', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, beerName: null, beerStyle: null, error: 'Error, please try again'});
} else {
res.render('index', { sessionID: req.sessionID, sessionExpireTime: expireTime, beersViewed: req.session.views, beerName: beerInfo.data.name, beerStyle: beerInfo.data.style, error: null});
}
}
});
});
What did we change? In router.get
, we added expireTime
so that we can calculate the amount of time until the session expires. Then in res.render
, we are
passing some additional values: the session ID from req.sessionID
, the expire time we just calculated, and the number of beers viewed per session, which is stored as req.session.views
.
On the first page view of a session, there will not be a value for req.session.views
, but our template knows how to handle that.
In router.post
, after we make the API request for the beer name, we are either incrementing req.session.views
or setting it to 1
if this is the first
beer name viewed in the session. Then, similar to what we saw above, we're passing along the additional session-related information in res.render
.
Session management in action!
With everything in place now, 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.
Click on the Pour Another button (within 60 seconds, so your session doesn't expire), and you should see that the session ID remains the same, and now you also see the number of beers viewed in the session set to 1
. If you open
dev tools in your browser and view cookies, you should see a cookie named _redisDemo
, and part of its value will contain the session ID.
Finally, if you start redis-cli
and then issue the following command, where YOUR_SESSION_ID
is replaced with the session ID shown in your browser,
you should see the session data that's being stored in Redis for that session, including the views.
$ redis-cli
$ get "sess:YOUR_SESSION_ID"
The output should look something like this:
Play around with the app some more to get a better understanding for how the sessions work. What happens if you close and then quickly re-open your browser? What happens if you wait more than 60 seconds and then refresh the page?
At this point, hopefully you have a better understanding of what session management is and how to implement it for a Node.js app using express-session
and connect-redis
. In Part 2, we'll build on what we've done in this tutorial by
adding authentication to the app using Passport.
Ready to add authentication to the app? Head over to Part 2.