Using secure client-side sessions to build simple and scalable Node.JS applications – A Node.JS Holiday Season, part 3

This is episode 3, out of a total 12, in the A Node.JS Holiday Season series from Mozilla’s Identity team. It covers using sessions for scalable Node.js applications.

Static websites are easy to scale. You can cache the heck out of them and you don’t have state to propagate between the various servers that deliver this content to end-users.

Unfortunately, most web applications need to carry some state in order to offer a personalized experience to users. If users can log into your site, then you need to keep sessions for them. The typical way that this is done is by setting a cookie with a random session identifier and storing session details on the server under this identifier.

Scaling a stateful service

Now, if you want to scale that service, you essentially have three options:

  1. replicate that session data across all of the web servers,
  2. use a central store that each web server connects to, or
  3. ensure that a given user always hits the same web server

These all have downsides:

  • Replication has a performance cost and increases complexity.
  • A central store will limit scaling and increase latency.
  • Confining users to a specific server leads to problems when that
    server needs to come down.

However, if you flip the problem around, you can find a fourth option: storing the session data on the client.

Client-side sessions

Pushing the session data to the browser has some obvious advantages:

  1. the data is always available, regardless of which machine is serving a user
  2. there is no state to manage on servers
  3. nothing needs to be replicated between the web servers
  4. new web servers can be added instantly

There is one key problem though: you cannot trust the client not to tamper with the session data.

For example, if you store the user ID for the user’s account in a cookie, it would be easy for that user to change that ID and then gain access to someone else’s account.

While this sounds like a deal breaker, there is a clever solution to work around this trust problem: store the session data in a tamper-proof package. That way, there is no need to trust that the user hasn’t modified the session data. It can be verified by the server.

What that means in practice is that you encrypt and sign the cookie using a server key to keep users from reading or modifying the session data. This is what client-sessions does.

node-client-sessions

If you use Node.JS, there’s a library available that makes getting started with client-side sessions trivial: node-client-sessions. It replaces Connect‘s built-in session and cookieParser middlewares.

This is how you can add it to a simple Express application:

const clientSessions = require("client-sessions");

app.use(clientSessions({
  secret: '0GBlJZ9EKBt2Zbi2flRPvztczCewBxXK' // set this to a long random string!
}));

Then, you can set properties on the req.session object like this:

app.get('/login', function (req, res){
  req.session.username = 'JohnDoe';
});

and read them back:

app.get('/', function (req, res){
  res.send('Welcome ' + req.session.username);
});

To terminate the session, use the reset function:

app.get('/logout', function (req, res) {
  req.session.reset();
});

Immediate revocation of Persona sessions

One of the main downsides of client-side sessions as compared to server-side ones is that the server no longer has the ability to destroy sessions.

Using a server-side scheme, it’s enough to delete the session data that’s stored on the server because any cookies that remain on clients will now point to a non-existent session. With a client-side scheme though, the session data is not on the server, so the server cannot be sure that it has been deleted on every client. In other words, we can’t easily synchronize the server state (user logged out) with the state that’s stored on the client (user logged in).

To compensate for this limitation, client-sessions adds an expiry to the cookies. Before unpacking the session data stored in the encrypted cookie, the server will check that it hasn’t expired. If it has, it will simply refuse to honour it and consider the user as logged out.

While the expiry scheme works fine in most applications (especially when it’s set to a relatively low value), in the case of Persona, we needed a way for users to immediately revoke their sessions as soon as they learn that they password has been compromised.

This meant keeping a little bit of state on the backend. The way we made this instant revocation possible was by adding a new token in the user table as well as in the session cookie.

Every API call that looks at the cookie now also reads the current token value from the database and compares it with the token from the cookie. Unless they are the same, an error is returned and the user is logged out.

The downside of this solution, of course, is the extra database read for each API call, but fortunately we already read from the user table in most of these calls, so the new token can be pulled in at the same time.

Learn more

If you want to give client-sessions a go, have a look at this simple demo application. Then if you find any bugs, let us know via our bug tracker.

Previous articles in the series

This was part three in a series with a total of 12 posts about Node.js. The previous ones are:

About Francois Marier

Security and Privacy Engineer

More articles by Francois Marier…

About Robert Nyman [Editor emeritus]

Technical Evangelist & Editor of Mozilla Hacks. Gives talks & blogs about HTML5, JavaScript & the Open Web. Robert is a strong believer in HTML5 and the Open Web and has been working since 1999 with Front End development for the web - in Sweden and in New York City. He regularly also blogs at http://robertnyman.com and loves to travel and meet people.

More articles by Robert Nyman [Editor emeritus]…


17 comments

  1. Caio Ribeiro Pereira

    Hi! This is a amazing post!

    I would like to know if I can use client-sessions with connect-redis (session/cookie middleware with redis) ?

    https://github.com/visionmedia/connect-redis

    December 5th, 2012 at 05:21

    1. François Marier

      I’ve never tried to use it with connect-redis, but if you do, please let us know how it went!

      December 5th, 2012 at 17:11

  2. Framp

    That’s a really neat module!

    I’ve just one question.. Couldn’t you just delete the server side key in order to destroy the session?
    The user (or the attacker in the compromised account scenery) will simply notice it’s been disconnected at the next request.

    December 5th, 2012 at 07:00

    1. François Marier

      Unless you use a second token like we did in Persona, there is no server-side key to delete because the session only exists on the client.

      December 5th, 2012 at 17:13

  3. David Mulder

    I have seen this set up a number of times before in the old days and personally I don’t like it (at all). There are a number of security related reasons for that: First of all I don’t believe that encryption should be the primary security mechanism of any system. Why? Because encryption has a habit of failing as time passes. This probably won’t hold true forever, but it has been true for quite a number of times now. For example, I don’t know whether you’re using a separate encryption key for each user or a shared server key, but the first would be ‘hackable’ (and I have seen that being done in real life for an important application, though it was relatively harder there to collect a large number of encrypted pieces of data). The second should be alright (for now). And either way, this is purely theoretical, but e.g. quantum computers will have a huge impact on various encryption schemes, and yet, it’s unlikely that this specific application will exist by then, but what I am trying to say is that an application should be fundamentally secure. And letting the user store secure data is *not* the way to make an application fundamentally secure.

    Oh and, besides that, I doubt that the overhead of a central server would be bigger than the overhead of sending all session data from the client to the server constantly (client upload is *slow* often).

    December 5th, 2012 at 13:31

    1. François Marier

      The server secret is the same for every user. That’s why you need to pick a reasonably long and random string for it. If you do that, then an attacker won’t be able to brute-force your key in any feasible timeframe.

      Of course, you can rotate the site secret at any point if you have reasons to believe it has been compromised. Doing so will invalidate all existing sessions.

      December 5th, 2012 at 17:18

    2. Simon

      I completely agree with David.

      Actually storing data on the client allows anyone to make massive brute force attack on the encrypted data.

      As a consequence the admin needs :
      – to setup a strong encryption (I would say 512 bits is a minimum)
      – to track the progress of technology to always keep ahead.
      – to hope there will be no sudden break through (Quantum CPU …) that destroy your encryption.

      And strong encryption has a cost on the CPU.

      January 15th, 2013 at 11:26

  4. Erlend Oftedal

    Client side sessions have two additional issues.
    1. If you store too much data in a session, you exceed the maximum header size an HTTP request can have. Thus every request fails, and recovery means closing the browser for most users.
    2. Clien side sessions are prone to race conditions. Request A is slow and while executibg request B comes by and modifies the session in it’s response. If A now finishes and also updates the sessions, it didnt see B’s change which is then lost.

    December 5th, 2012 at 15:50

    1. Lloyd Hilaiel

      Both of these issues are very real. We’ve hit #2 and it’s not fun to diagnose. You can mitigate this by path-scoping your cookies, striving for a one-page apps design, and being careful about needless session writes.

      But if you have to scale – eliminating the need to synchronize session data across colos with low latency requirements is huge in how it simplifies scaling.

      I’d say it would be hard to retrofit an existing application given these challenges – but we started very early in design with client side sessions, and given our scaling ambitions, for us it made good sense. YMMV!

      February 2nd, 2013 at 00:37

  5. gggeek

    Nice drawbacks highlighted by Erlend and Simon. There is a reason not so many sites implement client-side sessions…

    About the race condition described by Erlend: wouldn’t most web apps suffer from the same problem anyway? I imagine that the process answering to req. A only reads session data upon its start (from db to ram), and when it tries to update it at its end, it stores it (to DB) without checking if there was any update in-between. Unless there was some incrementing-counter stored as well along with the session data, incremented w. every update, which could be checked

    January 30th, 2013 at 13:16

    1. Erlend Oftedal

      Not necessarily. It depends on how the session is handled. I ran into this problem with a rails app that was updating the session very often. When I moved the session from client-side to in-memory, everything worked as expected.
      For an inmemory-store you might have other problems. One request may see an incosistent view of the session if another request is in the middle of updating it.
      For database-stored session you sessions can be handled correctly by transactions, but that may impact performance. Usually though database-stored session will also be cached, and can be updated without locking. In this case they may suffer from the same problems as the inmemory-store.

      In my opinion the probability of the race conditions due to multiple requests is greater than that of a dirty read.

      January 31st, 2013 at 02:47

      1. Lloyd Hilaiel

        Yes. I summarize it like this:

        server side sessions: session update happens as soon as a request hits the server.

        client side sessions: session update happens when a response is received.

        So the rule is – HTTP requests which may change the session cannot be parallelized – which can deeply impact frontend design.

        February 2nd, 2013 at 00:43

  6. NodeDude

    Nice module

    I’m not sold on encryption. Let’s say session lives 2 days, this is enough time to get together couple of amazon computational boxes and crack this mother.

    If I’m to guess, 50k $US buys you enough fire power to root ALL your users in 2 days (session expiry). This is risk not worth taking.

    March 13th, 2013 at 07:27

  7. Erlend

    That would depend on the encryption algorithm…
    http://www.eetimes.com/design/embedded-internet-design/4372428/How-secure-is-AES-against-brute-force-attacks-

    March 13th, 2013 at 08:52

  8. NodeDude

    I know close to nothing about encryption.

    I would really appreciate some detailed explaining or just some ramblings on viability of storing encrypted session on client side.

    Very attractive idea, but are there any production use cases?
    What encryption methods are available and optimal for this implementation?
    What decryption methods are available for chosen encryption strategies?

    This idea seems so risky yet so simple and clean… =)

    March 13th, 2013 at 11:36

  9. who did this

    Is it possible to set the cookies expiration into null after creation so I could offer both permanent and session cookies?

    March 20th, 2013 at 13:35

  10. Steven

    I had to think about a similar scheme. Instead of a token string I’d use a plain timestamp. Store the creation date in the cookie (I had to anyway) and a date in the user table. When the user clicks “revoke sessions” the date in the user table is updated to “now”. Compare the timestamps to see if the server should accept the cookie.

    April 3rd, 2013 at 08:52

Comments are closed for this article.