WittCode💻

Express Authentication with JWTs

By

Learn how to add protected routes to an express app by using JWT authentication. Specifically, we will be using the jsonwebtoken library from npm.

Table of Contents 📖

What is a JWT?

A JSON Web Token, or JWT, defines a way to securely transmit information between entities. This information is transmitted as a JSON object, hence the name JSON Web Token. JWTs are commonly used to implement authorization as they have small overhead and ensure integrity through signing of the token.

Express Application Setup

For this project, our Express application will have a route that simulates a login route and a protected route that requires a valid JWT.

import express, {ErrorRequestHandler, Request, Response} from 'express';
import JWTService from './JWTService.js';

const app = express();
app.use(express.json());
const PORT = 4001;

// login route
app.post('/login', (req, res) => {
});

// Verify token middleware
app.use((req: Request, res: Response, next) => {
});

// protected route
app.get('/protected', (req, res) => {
});

// Custom error handler
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  console.log(err);
  res.status(403).send('Invalid token!');
};
app.use(errorHandler);

app.listen(PORT, () => {
  console.log(`Server listening on port: ${PORT}`);
});

Placing the verify token middleware before the protected route ensures that it will be ran before the protected route. If an invalid token is received, then the error is caught by the custom error handler and a 403 response code is returned.

Creating a JWT

To create a JWT, we will use the jsonwebtoken library, an npm library that implements the JWT specification.

npm i jsonwebtoken

We will place all our JWT logic inside a file called JWTService. Inside this file, lets import the jsonwebtoken library and use it to generate a JWT.

import jwt from 'jsonwebtoken';

/**
 * JWTService
 */
export default class JWTService {
  private static SECRET = 'a09353a06dd0b7ea5832c9d57a1ff524b91b7f96048c75e58a5a8cda3aaf1f68';
  private static EXPIRATION = '3600s';

  /**
   * Generate a JWTF
   * @param data
   */
  generateJWT(userInfo: string | object | Buffer) {
    return jwt.sign(userInfo, JWTService.SECRET, {expiresIn: JWTService.EXPIRATION});
  }
}

The sign function used in the generateJWT method returns a JWT as a string. The arguments are as follows:

  • payload - The object that we want the JWT to contain, this will be non-sensitive user information
  • secret - The secret used to sign the token. This should be an environment variable, hidden from the public, and hard to guess.
  • options - used to configure the JWT. The expiresIn option mentions when the token expires or is no longer valid

JWT Structure

The JWT the sign method returns will be a string that looks like the following.

eyJhbGciOiJXVCJ9.eyJ1c2Vybmjk1MTIzfQ.f0CXG3XL7RmY

A JWT consists of 3 parts: a header, payload, and signature.

  • header - contains the token type and the signing algorithm used.
  • payload - contains the claims or statements about an entity. This is typically user related information.
  • signature - consists of the encoded header, payload, a secret, all hashed with the algorithm specified in the header

These 3 parts are then encoded and joined together by a dot to form the structure above.

Verifying a JWT

Now lets add a method to this class to verify a JWT. In other words, check that the provided token hasn't been forged or tampered with.

decodeJWT(token: string) {
  return jwt.verify(token, JWTService.SECRET);
}

The verify function returns the decoded JWT payload if the token is valid. The token is valid if it hasn't expired, the signature is valid, etc. If the token has been tampered with, the token won't be valid as the signature check will fail.

Creating a JWT with Express

Now lets fill in our login route to create a JWT when it receives a request.

app.post('/login', (req, res) => {
  // Create user
  const newUser = req.body;

  // Generate JWT
  const jWTService = new JWTService();
  const jwt = jWTService.generateJWT(newUser);
  res.status(200).send(jwt);
});

Note that in a real world scenario, the user first needs to login to the system successfully before accessing this route and obtaining a JWT. Now we simply need to send a cURL request to this route and look at the token returned.

curl -H "Content-Type: application/json" --request POST --data '{"username":"WittCode","id":1}' http://localhost:4001/login
eyJhbGciOiJIUzI1NiIkpXVCJ9.eyJ1c2VybmFtMjk0NzAzfQ.fX6KVZ-b-uAYD-AIPc-U

What we get back is the JWT token, which if decoded, will give us the user information.

Verifying a JWT with Express

Now lets fill in our JWT verification middleware. This middleware will be called before every route we want protected.

app.use((req: Request, res: Response, next) => {
  // Get JWT
  const authHeader = req.headers.authorization;
  const jwt = authHeader?.split('Bearer ')[1];

  // Get user from JWT
  const jWTService = new JWTService();
  const user = jWTService.decodeJWT(jwt!);

  // Attach user to locals
  res.locals.user = user;
  next();
});

For this middleware we use app.use as it is called every time a request is sent to the server. We first obtain the JWT from the Authorization header with Bearer schema and then decode it to get the user information. Afterwards, we attach it to the locals property of the response object. This property contains variables scoped to that specific request. Now lets fill in our protected route.

app.get('/protected', (req, res) => {
  const user = res.locals.user;
  res.send(user);
});

Now if our token is verified, it will be able to access this route, get the user from the locals property and return it. We can check this with a cURL command.

curl -H "Authorization: Bearer eyJhbGIkpXVCJ9.eyJ1cwIjoxzAzfQ.fX6K2dPc-U" localhost:4001/protected
{"username":"WittCode","id":1,"iat":1701343018,"exp":1701346618}

What we get back is our user information including the properties iat, or when the JWT was issued, and exp which is when the token expires. The token we apply to the Authorization header is the one that our verify token middleware will decode. If we provide an invalid token then we will hit our custom error handler and receive a 403.

curl -H "Authorization: Bearer faketoken.ohyeah.adsfadf" localhost:4001/protected
Invalid token!