It is a truth universally acknowledged that some kind of stateful sessions are indispensable to any website with users. I’m familiar with the Node/Express ecosystem, so I’ll write specifics about that. The theory and debates underlying session architecture should transfer to other languages.

  1. What is State?
  2. Serverful Sessions
  3. HTTP-Only Cookies as Primary Keys
  4. What goes in a session object?
  5. Server-Side Session Stores
  6. Configuring express-session
  7. Serverless Sessions and JSON Web Tokens
  8. The Great Debate: Cookies vs. Tokens
  9. Resources

What is State?

According to Wikipedia, a program is described as stateful if it is designed to remember preceding events or user interactions; the remembered information is called the state of the system.

So, are websites stateful? The answer is, not stateful enough for a login-protected user system.

Client-side, front-end websites are local storage, the browser object model (BOM), the document object model (DOM) it contains (BOM vs DOM), and whatever HTML, CSS, JavaScript, etc that the server serves to the BOM. The BOM and the DOM have state, but it resets every time you reload the page.

Server-side websites use data and algorithms to transform inputs into outputs. They, too, (at least Node.js) only have enough state for a single request-response cycle.

Serverful Sessions

Given that websites are not stateful between page reloads on the front and request-response cycles on the back, what can we do to serve the illusion of continuity (a logged in state) across a series of request-response-reload cycles? A session in Express is a (Javascript) object that stores information that persists across a series of requests from one device’s browser. Your code stores values in the session object that help you to match that device to a certain user and the choices they’ve made since logging in.

Sessions are not a built-in feature of Express; they require configuring the express-session middleware on your express server instance (application-level middleware). More about middleware. We’ll take a crack at that once we have a fuller picture of what we’re doing.

HTTP-Only Cookies as Primary Keys

If you’ve worked with SQL databases at all, you’re familiar with the concept of primary keys. If you’re not familiar, a primary key is a number or string attached to some data that allows you to pick that entry out and identify it in another context.

Modern, HTTP-only cookies are the (encrypted) primary keys of sessions. Each Express session has a random id called the sid. When the Express session middleware is configured, the browser makes its first query to the server. The server responds with whatever you res.send or res.render and also sends a set-cookie response header with the encrypted sid to the browser. Your server’s response renders in the browser with whatever information you sent. But when it requests the next resource from your server, it sends back the sid in a cookie request header. If the server receives a cookie request header from the browser, it doesn’t send the set-header response header back. Read more about HTTP headers. That cookie header tells the server that the client belongs to the session with the same sid, so the server can pull up that user’s info from its session data.

“Old-school” cookies are stored in the DOM as the document.cookies object. HTTP-only cookies are not accessible to Javascript. So assholes can steal old-school cookies with cross-site scripting attacks, but not HTTP-only cookies. It’s possible to intercept HTTP headers, but it’s harder.

What goes in a session object?

Never store passwords, hashes, credit card data, or anything else obviously sensitive in a session object. What should you store in the session object? Anything non-sensitive that helps you identify the user and their attributes on the client or the server across a series of requests. This will be things like associated database keys or _ids, names, emails, what they had for lunch, etc. Sensitive info can be pulled from the database or microservice fresh on demand using keys/_ids and/or environment variables. Since you don’t actually send the session object to the client, but just the cookie, any session info that you want to send to the client has to be send in the response itself; the cookie is just an identifier to connect the client browser with the server session object.

Server-Side Session Stores

Great! Express can keep track of who makes what request, and you can carry tidbits of info around the server with you for that person. But what if you are running a cluster of machines and one goes down? What if a different instance of the server receives the second request from a client?

Session stores to the rescue! The session store is a super simple table/collection/hashmap in virtually any type of database that saves each session object. When the client sends a cookie header to the server, if the session is not in the server’s working memory, it checks the session store for the de-encrypted sid. It typically only has a couple columns/document keys – one for the sid, and one for a (possibly stringified) JSON-like object that is the session object. Express session stores are available for MySQL, MongoDB, Redis, and more — check them out at the express-session github page.

Configuring express-session

Below is a snippet from an express server configuration.

server.js
console.log(`Node environment: ${process.env.NODE_ENV}`);

let dotenv;
if(process.env.NODE_ENV === 'development') {
	//if you're in development, use the development environment variables
	dotenv = require('dotenv').config({BASIC: 'basic', path: './env/.env.dev'});
} else if (process.env.NODE_ENV === 'test') {
	//if you're in test, use the test environment variables
	dotenv = require('dotenv').config({BASIC: 'basic', path: './env/.env.test'});
}
//if you're in production, the environment variables will be set on the production machine

//++++++ express config ++++++
//router and server
const express = require('express');
//package to parse requests sent to express
const bodyParser = require('body-parser');
//instantiate express
const app = express();

//set body parser on express middleware
app.use(bodyParser.urlencoded({
  extended: true
}));

app.use(bodyParser.json());


//serve the static assets from the public directory
app.use(express.static(__dirname + '/public'));

//++++++ logging config ++++++
//...

//++++++ database: mongoose connection for express sessions ++++++
//mongodb ORM
const mongoose = require('mongoose');
//Promise library for mongoose
const Promise = require('bluebird');
mongoose.Promise = Promise;

//create a mongoose connection from the connection address in the environment variable
mongoose.connect(process.env.MONGO_CONN, {
	useMongoClient: true
});
const conn = mongoose.connection;

//if there's a connection error, log it
conn.on("error", function(error) {
	console.log("the database connection farted! Here's how: " + error)
});
//when the connection opens, log it
conn.once("open", function() {
	console.log("the database is connected!");
});

//++++++ express sessions (remembering user data) ++++++

//session middleware package
const session = require('express-session');
//package to back sessions up to the database
const MongoStore = require('connect-mongo')(session);

//decode proxy value boolean from environment variable for options object below
let proxyValue = false;
if(process.env.PROXY_VALUE == '1') {
	//if there's a proxy, tell express to trust it.  used in production when serving node with nginx or similar
	app.set('trust proxy', 1);
	proxyValue = true;
}


//set session middleware options
app.use(session({
	//encrypt the cookie with the session secret environment variable
	secret: process.env.SESSION_SECRET,
	//if the session wasn't modified, don't save it again
	resave: false,
	//don't save sessions with nothing in them
	saveUninitialized: false,
	//set the value for proxy
	proxy: proxyValue,
	//cookie options
	cookie: {
		//must be served over https
		secure: true,
		//valid 3 days.  this is the true expiration setting
		maxAge: 1000 * 60 * 60 * 24 * 3,
		//i think there's some bug that specifying this fixes but pretty sure it's deprecated
		expires: false
	},
	//configure the session database collection connection and how long it retains sessions
	store: new MongoStore({
		mongooseConnection: conn,
		ttl: 60 * 60 * 24 * 3, //cookie valid 3 days
		//autoRemove: 'native'
	})
}));

//++++++ authentication setup ++++++
//...

//++++++ view engine setup +++++
//handlebars, pug, isomorphic react, etc

//++++++ ROUTES ++++++

const routes = require('./routes/routes');


//pass express the routes
app.use(routes);

//++++++ CONNECTION ++++++

const PORT = process.env.PORT || 3000;


//use https in development and testing, proxy http for production
if(process.env.NODE_ENV != 'production') {
	const fs = require('fs');
	const https = require('https');
	//self-generated, self-signed ssl certificates
	const privateKey  = fs.readFileSync('./ssl.key', 'utf8');
	const certificate = fs.readFileSync('./ssl.crt', 'utf8');
	
	const httpsServer = https.createServer({key: privateKey, cert: certificate}, app);
	
	httpsServer.listen(PORT, function() {
		console.log("express: secure development/test server listening to your mom on PORT: " + PORT);
	});
	
} else {
	app.listen(PORT, function() {
	  console.log("express: production server listening to your mom on PORT: " + PORT);
	});
}

To generate your own development SSL certificates, check out my post.

Serverless Sessions and JSON Web Tokens

Used to be, cookies were the only way. With the rise of serverless architectures, “server” functions are even more stateless because there is no there there. Nothing connects the requests except the client device making them. JSON web tokens (JWT) have become increasingly popular because they store session info on the client. JWT are sent to the client on authentication, often from a SaaS like Auth0, and stored in the browser (or on the phone) actually containing the data of the session within the token itself. Obviously they are encrypted. The client sends the token to the various serverless API endpoints to identify itself and persist the session for the client and the “server”. JWT have headers, a payload, and a cryptographic signature. Once you’ve verified them on the client side, you can store them in local storage or an old-school cookie, although they can be quite large and old-school cookies have a size limit. JWT also expire just like server-side sessions can.

The Great Debate: Cookies vs. Tokens

I’m not a security expert, but my personal preference is to use server-side sessions with a store and HTTP-only cookies if I actually have a server, and JWT if I don’t. Storing session data on the client feels ookie to me unless I absolutely have to. There is no definitive answer as of this writing whether cookies or tokens are more secure. Developers have strong feels for and against both. Google around for more opinions.

Any cookie or token should ALWAYS be sent over HTTPS.

Resources

  1. Express Session Docs
  2. Robert Hafner for Treehouse on Secure Cookies (2009)
  3. Robert Hafner for Treehouse on Secure Sessions (2009)
  4. ExpressJS Book on Sessions (2013)
  5. Jonathan Kresner Express Sessions Deep Dive (2015)
  6. Decembersoft on Authenticating a Session Cookie in Express with JWTs (2017?)
  7. Introduction to JWTs (2017)
  8. Auth0 (authentication as a service with JWTs)