Book Image

Building Scalable Apps with Redis and Node.js

By : Joshua Johanan
Book Image

Building Scalable Apps with Redis and Node.js

By: Joshua Johanan

Overview of this book

Table of Contents (17 chapters)
Building Scalable Apps with Redis and Node.js
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Using sessions in Express


Express uses the same methods, cookies, as most other web frameworks to track sessions. A cookie will have the session ID so that Express can look it up on each request.

Using cookies in Express

The latest version of Express has taken out much of the middleware that was previously included in Express. This is important when migrating from Express 3 to 4. We will use the cookie-parser package, which should already be installed. We will now add cookie parsing to our app. It is a middleware, so we will put it with all the other middleware. Remember that the middleware is processed in order, so that we can add each before the middleware that will actually use it, which is our routes. We will do this by adding a variable declaration and another function in the middleware stack.

//with all the other requires at the top of the file
var cookieParser = require('cookie-parser');
//in the middleware stack
app.use(express.static(__dirname + '/static'));
app.use(cookieParser());

We will repeat this pattern many times over this chapter and the next. Anytime there is require, it will be at the top of the file. The code that comes along with the book will also declare all the variables together. There will be one var, and each line will have require with a comma instead of a semicolon. Whether there is one var declaration or many, the code will still run. Further down in the file, we will use our new variable. I will try to include landmarks in the code to help, but we will add a lot of code in many places at times. Refer to the code that is supplied with the book.

The cookie parser gives us access to req.cookies. This will be an object that we can read the values out of. At this point, if we run our application, nothing will be different. We have not set cookies, nor are we trying to access them. Let's change that.

First, let's set up our views to show us the cookies in the request. In index.ejs under views, let's add the section. The file should look similar to the following code:

Index
<div>Cookie passed: <%= cookie %></div>

We now have to pass the cookie to our view. You should edit routes/index.js and add this to our view function. Note that we do not need to specify a layout because we have a default layout set with app.set('view options', {defaultLayout: 'layout'}).

exports.index = function index(req, res){
  res.render('index', {title: 'Index', cookie: JSON.stringify(req.cookies)});
};

Let's check it out in the browser. We should see that we have a blank cookie object in our request. It is time to create our own cookie. Open the JavaScript console in the browser (I am using Google Chrome) and type this in:

document.cookie="test=Test Cookie"

Refresh the page and see that it has our cookie in it. We can see that the request cookie object is just a simple JavaScript object, as seen in the following screenshot:

Next, we will set a cookie from the server. Express has a simple way to do this: in our index.js file under routes, let's add a cookie in the index function:

exports.index = function index(req, res){
  res.cookie('IndexCookie', 'This was set from Index');
  
res.render('index', {title: 'Index', cookie: JSON.stringify(req.cookies)});
};

Restart the node and load the page twice. The first restart will set the cookie, and the second restart will read it into our response. From the following screenshot, you should now see both cookies on our page:

You can also easily get rid of cookies by using clearCookie off the response object:

res.clearCookie('IndexCookie');

If you want to do it from the browser side, you can usually get a list of current cookies. In Chrome, this is in the developer tools. Click on the menu button in the upper right and navigate to Tools | Developer Tools. Then click on Resources | Cookies. You can then right-click on a specific cookie in the list and delete the cookie or select Clear All to delete all the cookies, as shown in the following screenshot:

By now, you should be feeling good about adding and removing cookies to requests and responses, so now let's see how to tie these cookies to a session.

Note

Hopefully, I have demonstrated how easily any attacker can forge cookies. Do not store sensitive information in your cookie. For example, storing a Boolean variable whether or not the user is logged in is a bad idea. We will shortly cover how to do all of this securely.

Adding sessions

Sessions allow us to store data about requests that are tied together with a cookie. HTTP is stateless, but cookies that map back to a session allow us to know that this is the same browser making multiple requests. You should be able to guess by now that Express comes with a great session middleware.

The first thing to know is that we need to store our sessions somewhere. For now, we will use a memory store.

You should add this to our variable declarations at the top of app.js:

var session = require('express-session');

Next, add the middleware. You should remember to add it under our cookieParser middleware, as follows:

app.use(cookieParser());
app.use(session());

The express session uses cookies, so the cookie object needs to be present before it can use the session.

Now, we can use our session. We will update our index page to show what is stored in our session. Edit index.ejs under views to display a session:

Index
<div>Cookie passed: <%= cookie %></div>
<div>Session: <%= session %></div>

The session middleware adds a new object to our request, which is req.session. Let's pass this to the view from index.js under middleware:

function index(req, res){
  res.cookie('IndexCookie', 'This was set from Index');
  res.render('index', {title: 'Index', cookie: JSON.stringify(req.cookies), session: JSON.stringify(req.session)});
};

Once you load this up, you will find that we get an error. If we check our console, as you can see from the following screenshot, we need to add a secret option for sessions:

We can now do this by revisiting our session middleware and adding a secret option:

app.use(session({secret: 'secret'}));

The secret option uses the string we pass in to create a hash of our session ID, so we can tell if someone has tried to tamper with our cookie (also known as a request forgery). We just covered ways by which users can easily delete and create any cookie that they want. If our cookie had a session ID in it, which for example could be 1234, a user could delete that cookie and create a new one with a session ID of 1235. As far as the server knows, the next request comes from the user who has session 1235. A hashed session ID makes this much more difficult. If the user does not know the secret (don't actually use secret or 123456, use something such as http://randomkeygen.com/ or http://www.guidgenerator.com/ to get a unique secure secret), then their ability to create a valid token is reduced. This is a very contrived example, but it should illustrate why we need this.

Reload the node and refresh twice. We can now see our session and the cookie that was created in the following screenshot:

We can test our security by deleting our connect.sid cookie and creating a new one. On the next request, we will get a new connect.sid cookie set.

Let's build a simple page counter in the session. On each request, we will increment a counter. We can do this easily by adding a middleware function. We only need to remember to add it under the session middleware so that we have access to req.session; we will write this function inline as we are not going to keep it in our final middleware stack. Add this to the stack right under session:

app.use(function(req, res, next){
  if(req.session.pageCount)
    req.session.pageCount++;
  else
    req.session.pageCount = 1;
  next();
});

Test it by going around and loading a bunch of pages. The pageCount session variable should track each different request you make. The request could be a 404 or even an error. Our middleware runs and adds to the total before any error handling. One thing to remember is that only our index view will output pageCount. After testing this, we can remove the middleware.

One limitation to how we have set this up is that only the node instance that created the session also has access to it. If you run multiple node instances, you will need to have a different session store from memory.

Redis as a session store

Redis is an in-memory key-value store. We will use Redis to hold the session ID as a key and the session data as a value. It is important to note that we will not get into what Redis is and how to install it here as Chapter 5, Adopting Redis for Application Data, will cover the topic. Also, we will not cover the security issues with Redis now as we just want to get it working for our sessions. However, we will cover how to add it as an Express session store.

We will use the two packages redis and connect-redis. To use a Redis store, we assume that we are running Redis locally and that Redis' version is above 2.0.0 (the latest version, as of writing this book, is 2.8.6, so this isn't a huge hurdle). First, let's change our reference to the memory store so that our variable session will point to a connect-redis instance. Change these variable declarations in app.js:

var session = require('express-session');
var RedisStore = require('connect-redis')(session);

Connect-redis extends the session. We can now set up our middleware. Change our session middleware to this:

app.use(session({
  secret: 'secret',
  saveUninitialized: true,
  resave: true,
  store: new RedisStore(
    {url: 'redis://localhost'})
  })
);

We use the same secret, but we will now create a new RedisStore object with an options object using the Redis server's URL. This URL can take a username, password, and port, if all of these were not the default values. At this point, we can restart our server and load up our index page. It should be working in exactly the same way as it was with an in-memory store. We also have a couple of other options. If these are not added, a warning is thrown.

Let's actually take a peek into what is happening here. We know at this point that our session is tracked with a cookie, but unfortunately, this is a signed value. We can get access to this by changing our cookieParser middleware to use the same secret as the session middleware. The following line of code is what our new cookieParser line should look like:

app.use(cookieParser('secret'));

Remember that the secret passed must match the one used for the session. This is because the session middleware creates the cookie and the cookieParser middleware reads it out. We will now have req.signedCookies. Any signed cookie will be here, so it is time to test this out. We will need to update index.ejs in the View folder and index.js in the routes folder provided in the code bundle.

The index.ejs file in the views folder looks like:

Index
<div>Cookie passed: <%= cookie %></div>
<div>Signed Cookie passed: <%= signedCookie %></div>
<div>Session: <%= session %></div>

The index.js file in the routes folder looks like:

exports.index = function index(req, res){
  res.cookie('IndexCookie', 'This was set from Index');
  res.render('index', {title: 'Index', 
    cookie: JSON.stringify(req.cookies), 
    session: JSON.stringify(req.session), 
    signedCookie: JSON.stringify(req.signedCookies)});
};

From the following screenshot, you can see that our unsigned cookies will be first and our connect.sid cookie will be second:

The browser will still get the signed cookie, as you can see in the following screenshot:

Without getting too much into Redis, we will look up our session in Redis. We can quickly install Redis on Mac OS X by running the following command:

brew install redis

We can then launch redis-cli (which we should now have if we have Redis installed. If you face issues, jump to Chapter 5, Adopting Redis for Application Data). We can now run a command against Redis. connect-redis will prepend sess: to all the session keys in Redis. To see our session, we will run GET sess:YOUR-SESSION-ID, as shown in the following command line:

$ redis-cli
127.0.0.1:6379> GET sess:0DMsXhobExvbCL3FFeYqRGWE
"{\"cookie\":{\"originalMaxAge\":null,\"expires\":null,\"httpOnly\":true,\"path\":\"/\"}}"

We can see that this returns our session object as an escaped string. You can compare this to the object that was returned from our response and see that it is the same. We have successfully moved our sessions to a data store that can be accessed from multiple servers. One of the most basic ideas of creating a scalable application is not to keep any shared state on the local instance. Previously, with the memory store for sessions, each server had its own state. Now, we can have multiple servers share the state. Here, we are using Redis, but you can use any data store to do this (which is not limited to memcache, MongoDB, Postgres, and many others). We are not going to do this here in this chapter, but we have started to prepare our app to be scalable. Another thing to note is that the Redis server is running on localhost. For a production-ready scalable application, Redis will be moved to a separate server or even several servers.

Let's clean up our views a little. You definitely do not want to send all of a user's session data to them. In index.ejs in the views folder, remove everything except for Index. In index.js in the routes folder, drop all the other attributes except for title, and remove the line that sets the cookie. This is shown as follows:

exports.index = function index(req, res){
  res.render('index', {title: 'Index'});
};