Cookies, Sessions and JWT: The Very, Very Basics

Give me a quick overview of identifying client browsers please!

Milk and cookies
Cookies, sessions and JWT tokens

In this article we will be referring exclusively to HTTP cookies, and so all references to ‘cookie’ will be within this context.

Audience

This article is aimed at developers with a minimal understanding of HTTP. It looks to clarify some basic questions surrounding cookies, sessions and JWT, finishing with a worked example using Express.js.

Argument

Within this article we will explore the definition of a cookie, what they contain and how they are related to sessions and JWT tokens. We will then work through these concepts using an example.

Cookies are simply text files containing small amounts of data. They are primarily used to identify your computer amongst all of the others sending information to a server. However, they often double as a way of storing information about your browsing preferences.

They are automatically stored in the memory of your browser, and tend to contain:

  • The name of the server the cookie originated from.
  • The lifetime of the cookie (how long it remains valid).
  • The value of the cookie. This is what the server is interested in.

Accessing all of the cookies on your machine is actually very easy, and we demonstrate it below using Chrome:

An example cookie

In the above we see an example cookie that has been sent to our browser from www.website.com. It is valid for 24 hours from the given date and stores a seemingly random value. In reality this could represent a session ID (covered in more detail later).

There are two types of cookie: session and persistent.

  • Session cookies are temporary and are erased when you close your browser tab or window. They are used to track your movement from page to page.
  • Persistent cookies remain stored until they are erased or they expire. We can see in our example we have a persistent cookie as an expiration time has been set. These are often used for authentication or stored preferences.

Conceptually they are quite similar, they are both used to store information around a user’s activity. One difference is that cookies are stored on the client’s browser, however sessions are stored on the server. Another is that cookies have the option to be persistent, however sessions do not. A final point worth noting is cookies are limited to 4KB in size, so if we have a lot of session data to store, then sessions are preferred.

Cookies are still crucial to sessions as they store the session ID, this is how the server relates a particular session to a particular client browser.

Cookies are sent to the client using a Set-Cookie header. For example, we may make a request similar to the below:

HTTP/2.0 200 OK
Content-Type: application/json
Set-Cookie: cookie_one=value_one
Set-Cookie: cookie_two=value_two

...some JSON payload

Then whenever the client makes requests to the server it uses the previously stored cookies in theCookie header.

GET /index.html HTTP/2.0
Host: www.website.com
Cookie: cookie_one=value_one; cookie_two=value_two

JWT Tokens are encoded, cryptographically signed JSON data. The idea is that this is JSON guaranteed to come from a certain source.

These are commonly used for authentication, as demonstrated below.

A simplified authentication choreography using JWT

A client application is authorised by our authorisation service, which returns a JWT token. This JWT token is then sent to our API using the Authorisation header and Bearer schema as part of the HTTP request.

Authorization: Bearer <token>

So what do these tokens look like? An example JWT token is explained below.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

This essentially consists of three parts, separated by a .

The header: This comprises is the algorithm used to sign the JSON, and the type of token (JWT in our case).

{
"alg": "HS256",
"typ": "JWT"
}

The payload: This contains the ‘claims’. Claims are statements about an entity, as well as some additional data. There are three types of claims: registered, public, and private claims.

  • Registered claims are a set of recommended predefined claims, for example: iss (issuer) and exp (expiration time).
  • Public claims are claims registered by a JWT-external body in the IANA JSON Web Token Claims Registry. Following the link we can see that OpenID Connect has registered some public claims.
  • Private claims are decided by two parties, and are neither registered nor public.

An example payload is found below.

{
"sub": "123456789",
"name": "John Smith",
"iat": 1516239021
}

The signature: To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and combine them.

signingAlgorithm(
base64UrlEncodedHeader + "." +
base64UrlEncodedPayload,
secret)

This signature can then be used to verify the message hasn’t changed, and if the secret is a private key, we can also use it to verify the sender!

An important point to note is that these tokens are still visible to everyone, we can just guarantee they haven’t been tampered with. So don’t put anything sensitive in them!

Before we move on to our worked example, lets summarise our findings so far:

  • Cookies are small text files stored on the browser and are used to identify a client.
  • Sessions are stored on the backend, but may use cookies to store a session Id in order to relate a client to a session.
  • JWT tokens can be used with authentication and are generally not stored at all, instead they are passed with requests.

The mechanism for cookies and sessions is superficially very simple, and is heavily aided by modern frameworks. We will be exploring this using an example Express server, which will provide a simple (and not at all realistic) site for buying apples.

JWT tokens will be left for another day, although if you feel motivated to investigate further a useful tool can be found here.

The example repository can be found here. I have generated a blank express project using the express generator project and the command:

express sessions

This creates a working express app in the sessions directory. We now install the express-sessions middleware using:

npm install express-session

The express generator file has created an app.js file with our Express code in. At the top of it we will add our new session dependency.

var session = require('express-session')

We can then configure the express app to use the session middleware:

app.use(session({
secret: 'This is my secret',
resave: false,
saveUninitialized: true
}))

Although there is much more comprehensive documentation, we have chosen to use the minimum setup required.

  • Secret is the secret we use in signing our session cookie.
  • Resave forces the session to be saved to the session store after a request, even if the session wasn’t changed.
  • Save Uninitialised forces a session that is new but not modified to be saved to the store.

From here we will add three new pages to the application: login, apple and checkout.

The login.js file is below. It sets a flag on the session to say that we are logged in, then renders a logged in page.

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
console.log(`Logging in, session Id ${req.session.id}`);
req.session.loggedIn = true;
res.render('login');
});
module.exports = router;

We also have a login.jade (don’t ask) page, which provides the view.

extends layoutblock content
h1 Log In
p You are now logged in
a(href="/")="Home"

The apple.js file is below. It is used to add an extra apple to our apple cart. You can see that if a user’s session shows they are not logged in they will be asked to do so.

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
if(!req.session.loggedIn) {
res.render('notLoggedIn');
}

console.log(`Adding apple to cart, session Id ${req.session.id}`);
if(req.session.appleCount) {
req.session.appleCount++
} else {
req.session.appleCount = 1
}
res.render('apple', { count: req.session.appleCount });});module.exports = router;

We also have an apple.jade file which is rendered when we add an apple to our cart.

extends layoutblock content
h1 Number of Apples
p You have #{count} apples!
a(href="/")="Home"

The checkout.js file is below. When we have added enough apples we can go to the checkout and pay for them.

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
if(!req.session.loggedIn) {
res.render('notLoggedIn');
}
console.log(`At the checkout, session Id ${req.session.id}`); res.render('checkout', { count: req.session.appleCount });});module.exports = router;

Here is the checkout.jade file to be rendered:

extends layoutblock content
h1 Checkout
p You paid for your #{count} apples!

a(href="/")="Home"

There is also a notLoggedIn.jade file which is used in the cases where a client should be logged in, but is not.

extends layoutblock content
h1 Error!
p You are not logged in
a(href="/")="Home"

We wire all of these up in the app.js using the below:

var loginRouter = require('./routes/login');
var appleRouter = require('./routes/apple');
var checkoutRouter = require('./routes/checkout');
...app.use('/login', loginRouter);
app.use('/apple', appleRouter);
app.use('/checkout', checkoutRouter);

When we start up the application we are greeted with the home screen.

The Apple Store home screen

If we go to buy an apple without logging in we receive the below.

Our sessions shows we are not logged in

Going back to the home screen and logging in we have our successful login screen.

A successful login screen

We can then go and add as many apples as we like, all of which are stored in our session.

Adding apples to our cart

The final thing we do is navigate to the checkout to pay.

Paying for the apples

Play around with new tabs and windows, checking to see their effect on the store.

The final thing to do is to look at the cookie used to store our session in the browser.

Our session cookie, stored in the browser.

Conclusion

To conclude, we have examined cookies, sessions and JWT tokens, explaining their various uses. We have also demonstrated a modern implementation using Express.

Principal Software Engineer at the BBC