In this post, we will demonstrate how JWT(JSON Web Token) based authentication works, and how to build a sample application in Node.js to implement it.

banner

If you already know how JWT works, and just want to see the implementation, you can skip ahead, or see the source code on Github

The JSON web token (JWT) allows you to authenticate your users in a stateless manner, without actually storing any information about them on the system itself (as opposed to session based authentication).

The JWT Format

Consider a user called user1, trying to login to an application or website: Once they’re successful they would receive a token that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54

This is a JWT, which is made up of three parts (separated by .):

  1. The first part is the header (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9). The header specifies information like the algorithm used to generate the signature (the third part). This part is pretty standard and is the same for any JWT using the same algorithm.
  2. The second part is the payload (eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ), which contains application specific information (in our case, this is the username), along with information about the expiry and validity of the token.
  3. The third part is the signature (2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54). It is generated by combining and hashing the first two parts along with a secret key.

Note that the header and payload are not encrypted – They are just base64 encoded. This means that anyone can decode them using a base64 decoder

For example, if we decode the header to plain text, we will see the below content:

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

 

If you are using linux or Mac OS, you can also execute the following statement on the terminal:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d

Similarly, the contents of the payload are:

{ "username": "user1", "exp": 1547974082 }

How the JWT Signature Works

So if the header and signature of a JWT can be accessed by anyone, what actually makes a JWT secure? The answer lies in how the third portion (the signature) is generated.

Consider an application that wants to issue a JWT to a user (for example, user1) that has successfully signed in.

Making the header and payload are pretty straightforward: The header is fixed for our use case, and the payload JSON object is formed by setting the user ID and the expiry time in unix milliseconds.

The application issuing the token will also have a key, which is a secret value, and known only to the application itself.

qThe base64 representations of the header and payload are then combined with the secret key and then passed through a hashing algorithm (in this case its HS256, as mentioned in the header)

jwt algorithm

The details of how the algorithm is implemented is out of scope for this post, but the important thing to note is that it is one way, which means that we cannot reverse the algorithm and obtain the components that went into making the signature - so our secret key remains secret.

Verifying a JWT

To verify a JWT, the server generates the signature once again using the header and payload from the incoming JWT, and its secret key. If the newly generated signature matches the one on the JWT, then the JWT is considered valid.

Now, if you are someone trying to issue a fake token, you can easily generate the header and payload, but without knowing the key, there is no way to generate a valid signature. If you try to tamper with the existing payload of a valid JWT, the signatures will no longer match.

jwt verification

In this way, the JWT acts as a way to authorize users in a secure manner, without actually storing any information (besides the key) on the issuing server.

Implementation in Node.js

Now that we’ve seen how JWT based authentication works, let’s implement it using Node.

Creating the HTTP Server

Let’s start by initializing the HTTP server with the required routes in the index.js file. We’ve used express as the server framework:

const express = require("express")
const bodyParser = require("body-parser")
const cookieParser = require("cookie-parser")

const { signIn, welcome, refresh } = require("./handlers")

const app = express()
app.use(bodyParser.json())
app.use(cookieParser())

app.post("/signin", signIn)
app.get("/welcome", welcome)
app.post("/refresh", refresh)

app.listen(8000)

We can now define the signIn and welcome routes.

Handling User Sign In

The /signin route will take the users credentials and log them in. For simplification, we’re storing the users information as an in-memory map in our code:

const users = {
	user1: "password1",
	user2: "password2",
}

So for now, there are only two valid users in our application: user1, and user2. Next, we can write the signIn HTTP handler in a new file handlers.js. For this example we are using the jsonwebtoken library to help us create and verify JWT tokens.

const jwt = require("jsonwebtoken")

const jwtKey = "my_secret_key"
const jwtExpirySeconds = 300

const users = {
	user1: "password1",
	user2: "password2",
}

const signIn = (req, res) => {
	// Get credentials from JSON body
	const { username, password } = req.body
	if (!username || !password || users[username] !== password) {
		// return 401 error is username or password doesn't exist, or if password does
		// not match the password in our records
		return res.status(401).end()
	}

	// Create a new token with the username in the payload
	// and which expires 300 seconds after issue
	const token = jwt.sign({ username }, jwtKey, {
		algorithm: "HS256",
		expiresIn: jwtExpirySeconds,
	})
	console.log("token:", token)

	// set the cookie as the token string, with a similar max age as the token
	// here, the max age is in milliseconds, so we multiply by 1000
	res.cookie("token", token, { maxAge: jwtExpirySeconds * 1000 })
	res.end()
}

If a user logs in with the correct credentials, this handler will then set a cookie on the client side with the JWT value. Once a cookie is set on a client, it is sent along with every request henceforth. Now we can write our welcome handler to handle user specific information.

Handling Post Authentication Routes

Now that all logged in clients have session information stored on their end as cookies, we can use it to:

  • Authenticate subsequent user requests
  • Get information about the user making the request

Let’s write our welcome handler in handlers.js to do just that:

const welcome = (req, res) => {
	// We can obtain the session token from the requests cookies, which come with every request
	const token = req.cookies.token

	// if the cookie is not set, return an unauthorized error
	if (!token) {
		return res.status(401).end()
	}

	var payload
	try {
		// Parse the JWT string and store the result in `payload`.
		// Note that we are passing the key in this method as well. This method will throw an error
		// if the token is invalid (if it has expired according to the expiry time we set on sign in),
		// or if the signature does not match
		payload = jwt.verify(token, jwtKey)
	} catch (e) {
		if (e instanceof jwt.JsonWebTokenError) {
			// if the error thrown is because the JWT is unauthorized, return a 401 error
			return res.status(401).end()
		}
		// otherwise, return a bad request error
		return res.status(400).end()
	}

	// Finally, return the welcome message to the user, along with their
	// username given in the token
	res.send(`Welcome ${payload.username}!`)
}

Renewing Your Token

In this example, we have set a short expiry time of five minutes. We should not expect the user to login every five minutes if their token expires.

To solve this, we will create another /refresh route that takes the previous token (which is still valid), and returns a new token with a renewed expiry time.

To minimize misuse of a JWT, the expiry time is usually kept in the order of a few minutes. Typically the client application would refresh the token in the background.

const refresh = (req, res) => {
	// (BEGIN) The code uptil this point is the same as the first part of the `welcome` route
	const token = req.cookies.token

	if (!token) {
		return res.status(401).end()
	}

	var payload
	try {
		payload = jwt.verify(token, jwtKey)
	} catch (e) {
		if (e instanceof jwt.JsonWebTokenError) {
			return res.status(401).end()
		}
		return res.status(400).end()
	}
	// (END) The code uptil this point is the same as the first part of the `welcome` route

	// We ensure that a new token is not issued until enough time has elapsed
	// In this case, a new token will only be issued if the old token is within
	// 30 seconds of expiry. Otherwise, return a bad request status
	const nowUnixSeconds = Math.round(Number(new Date()) / 1000)
	if (payload.exp - nowUnixSeconds > 30) {
		return res.status(400).end()
	}

	// Now, create a new token for the current user, with a renewed expiration time
	const newToken = jwt.sign({ username: payload.username }, jwtKey, {
		algorithm: "HS256",
		expiresIn: jwtExpirySeconds,
	})

	// Set the new token as the users `token` cookie
	res.cookie("token", newToken, { maxAge: jwtExpirySeconds * 1000 })
	res.end()
}

We’ll need to export the handlers at the end of the file:

module.exports = {
	signIn,
	welcome,
	refresh
}

Handling Logout

Logging out can be tricky when it comes to JWT-based authentication, since our application is meant to be stateless - meaning we don’t store any information about issued JWT tokens on our server.

The only information we do have is our secret key and algorithm used to encode and decode the JWT. If a token satisfies these requirements, it is considered valid by our application.

This is why the recommended way to handle logout is to provide tokens with a short expiry time, and require the client to keep refreshing the token. This way, we can ensure that for an expiry period T, the maximum time a user can stay logged in without the applications explicit permission is T seconds.

Another option we have is to create a /logout route that clears the users token cookie, so that subsequent requests would be unauthenticated:

const logout = (req, res) => {
  res.cookie('token', '', { maxAge: 0 })
  res.end()
}

However, this is a client side implementation, and can be circumvented if the client decides not to follow instructions and delete the cookie.

We can also store JWTs that we want to invalidate on the server, but this would make our application stateful.

Running Our Application

To run this application, run the command:

node ./index

Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:

POST http://localhost:8000/signin

{"username":"user1","password":"password1"}

You can now try hitting the welcome route from the same client to get the welcome message:

GET http://localhost:8000/welcome

Result:

Welcome user1!

Hit the refresh route, and then inspect the clients cookies to see the new value of the token cookie:

POST http://localhost:8000/refresh

You can find the working source code for this example here.

If you want to learn more about cryptography in Node.js, I’ve written another post on implementing RSA encryption in Node.js