JSON web tokens

JSON Web Tokens (JWT) have become a standard method for securing web applications and APIs. JWT is an open, industry-standard RFC 7519 method for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs are compact, URL-safe, and can be used for a variety of purposes, including authentication and information exchange.

JSON Web Tokens

What is a JWT?

A JSON Web Token (JWT) is a compact and self-contained token that is used for securely transmitting information between parties as a JSON object. It is commonly used in authentication and authorization mechanisms.

A JWT consists of three parts:

  1. Header: Contains the metadata about the token, such as the type of token (JWT) and the signing algorithm (e.g., HS256).
  2. Payload: Contains the claims, which are statements about an entity (usually the user) and additional data.
  3. Signature: A cryptographic signature that is created by combining the encoded header and payload and signing it with a secret key or a public/private key pair.

A JWT is structured as follows:

				
					header.payload.signature

				
			

How JWT Works

The process of using JWT typically involves the following steps:

  1. User Authentication: The user logs in with their credentials (e.g., username and password).
  2. JWT Generation: The server validates the credentials and generates a JWT, which is sent back to the client.
  3. Token Storage: The client stores the JWT (usually in local storage or a cookie).
  4. Token Usage: The client sends the JWT in the Authorization header of subsequent requests to access protected routes.
  5. Token Verification: The server verifies the JWT in each request to ensure the user is authorized to access the requested resource.

JWT Structure

Header

The header typically consists of two parts:

  • alg: The signing algorithm being used (e.g., HS256).
  • typ: The type of the token, which is JWT.

Example header:

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

				
			

Payload

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims:

  • Registered Claims: Predefined claims such as iss (issuer), exp (expiration time), sub (subject), and aud (audience).
  • Public Claims: Custom claims defined by those using JWTs.
  • Private Claims: Custom claims that are shared between parties that agree to use them.

Example payload:

				
					{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

				
			

Signature

To create the signature, you need to take the encoded header, the encoded payload, a secret, and the algorithm specified in the header.

Example signature creation:

				
					HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

				
			

The resulting JWT looks something like this:

				
					eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

				
			

Implementing JWT in Express.js

Setting Up the Project

To demonstrate JWT implementation in an Express.js application, we’ll start by setting up a simple project.

File: app.js

				
					const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;
const SECRET_KEY = 'your-secret-key';

app.use(bodyParser.json());

				
			

Explanation:

  • Express Setup: We’re initializing an Express.js application.
  • JWT Module: We’re using the jsonwebtoken package for handling JWTs.
  • SECRET_KEY: This is the secret key used to sign the JWTs. In a real application, this should be stored securely and not hard-coded.

Creating a JWT on User Login

When a user logs in, the server will authenticate the user and generate a JWT.

File: app.js

				
					// Simulated user data
const users = [
  { id: 1, username: 'john', password: 'password123', role: 'admin' },
  { id: 2, username: 'jane', password: 'password456', role: 'user' }
];

// Login route
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Find user
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).send('Invalid credentials');
  }

  // Generate JWT
  const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: '1h' });

  res.json({ token });
});

				
			

Explanation:

  • User Data: For simplicity, we’re using hard-coded user data. In a real application, you’d query your database.
  • Login Route: The user sends their credentials to /login. If the credentials are valid, we generate a JWT using jwt.sign() and send it back to the client.
  • Token Payload: The payload includes the user’s ID, username, and role.
  • Token Expiration: The token expires in 1 hour.

Output:

1.Sending a POST request to /login with valid credentials (e.g., { "username": "john", "password": "password123" }) returns a JWT:

				
					{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJqb2huIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjI3NjI3OTI5LCJleHAiOjE2Mjc2MzE1Mjl9.8c5ff8v8rX3LPk-4ndPHnbmav5Cbbbl8pG6CfOEJcgg"
}

				
			

2.Sending a POST request with invalid credentials returns:

				
					Invalid credentials

				
			

Securing Routes with JWT Middleware

After logging in and receiving a JWT, the client will include this token in the Authorization header of requests to access protected routes.

File: app.js

				
					// Middleware to verify JWT
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// Protected route
app.get('/dashboard', authenticateToken, (req, res) => {
  res.json({ message: `Welcome, ${req.user.username}`, role: req.user.role });
});

				
			

Explanation:

  • authenticateToken Middleware: This middleware checks if a JWT is present in the Authorization header and verifies it using jwt.verify(). If valid, the decoded user data is attached to req.user and the request proceeds. If the token is invalid or missing, the request is denied.
  • Protected Route: The /dashboard route is protected by the authenticateToken middleware. Only requests with a valid JWT can access this route.

Output:

  1. Sending a GET request to /dashboard with a valid JWT in the Authorization header returns
				
					{
  "message": "Welcome, john",
  "role": "admin"
}

				
			

2.Sending a request without a token or with an invalid token returns a 401 or 403 status.

Role-Based Authorization with JWT

You can extend the JWT implementation to enforce role-based access control (RBAC) by checking the user’s role.

File: app.js

				
					// Role-based access control middleware
function authorizeRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).send('Access denied: insufficient privileges');
    }
    next();
  };
}

// Admin-only route
app.get('/admin', authenticateToken, authorizeRole('admin'), (req, res) => {
  res.json({ message: 'Welcome to the admin area' });
});

				
			

Explanation:

  • authorizeRole Middleware: This middleware checks if the authenticated user has the required role. If not, access is denied.
  • Admin Route: The /admin route is only accessible to users with the ‘admin’ role.

Output:

  1. Accessing /admin with an admin token returns:
				
					{
  "message": "Welcome to the admin area"
}

				
			

2.Accessing /admin with a non-admin token returns:

				
					Access denied: insufficient privileges

				
			

Advanced JWT Concepts

Refresh Tokens

JWTs are often short-lived for security reasons. A refresh token is a long-lived token that can be used to obtain a new JWT without requiring the user to log in again.

Implementing Refresh Tokens

Refresh tokens are usually stored securely on the client side (e.g., in a cookie or local storage).

File: app.js

				
					let refreshTokens = [];

// Generate and store refresh token
app.post('/token', (req, res) => {
  const { token } = req.body;
  if (!token) return res.sendStatus(401);
  if (!refreshTokens.includes(token)) return res.sendStatus(403);

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) return res.sendStatus(403);
    const accessToken = jwt.sign({ id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: '15m' });
    res.json({ accessToken });
  });
});

// Revoke refresh token
app.post('/logout', (req, res) => {
  refreshTokens = refreshTokens.filter(token => token !== req.body.token);
  res.sendStatus(204);
});

				
			

Explanation:

  • Token Route: The /token route generates a new access token using a valid refresh token.
  • Logout Route: The /logout route invalidates the refresh token by removing it from the stored list.

Output:

  1. Sending a POST request to /token with a valid refresh token returns a new access token.
  2. Sending a POST request to /logout with a valid refresh token removes it from the list, preventing future token refreshes.

JWT Blacklisting

JWTs are stateless by design, which means the server does not keep track of issued tokens. However, if you need to invalidate a specific token (e.g., during a logout), you can implement a blacklist.

Implementing JWT Blacklisting

You can store blacklisted tokens in a database or an in-memory store.

File: app.js

				
					let blacklistedTokens = [];

// Blacklist token
app.post('/blacklist', authenticateToken, (req, res) => {
  const token = req.headers['authorization'].split(' ')[1];
  blacklistedTokens.push(token);
  res.sendStatus(204);
});

// Middleware to check blacklist
function checkBlacklist(req, res, next) {
  const token = req.headers['authorization'].split(' ')[1];
  if (blacklistedTokens.includes(token)) return res.sendStatus(403);
  next();
}

// Apply blacklist check to protected routes
app.use('/dashboard', checkBlacklist);
app.use('/admin', checkBlacklist);

				
			

Explanation:

  • Blacklist Route: The /blacklist route adds the token to the blacklist.
  • checkBlacklist Middleware: This middleware checks if the token is blacklisted before allowing access to protected routes.

Output:

  1. Blacklisting a token prevents it from accessing protected routes.
  2. Trying to access /dashboard or /admin with a blacklisted token returns a 403 status.

Best Practices for JWT

Use Short Expiration Times for Access Tokens

Short expiration times reduce the risk of a compromised token being used maliciously. Use refresh tokens to extend user sessions securely.

Securely Store Refresh Tokens

Store refresh tokens securely, preferably in an HTTP-only cookie. Avoid storing them in local storage to prevent XSS attacks.

Use HTTPS

Always use HTTPS to transmit tokens securely over the network. This prevents tokens from being intercepted by attackers.

Implement Token Revocation

Implement mechanisms like refresh tokens and blacklisting to revoke tokens when necessary, such as during logout or if a token is compromised.

Handle Token Expiration Gracefully

When a token expires, inform the user and provide a seamless way to refresh the token or re-authenticate.

Practical example

In this section, we will create a simple Express.js application that demonstrates how to use JSON Web Tokens (JWT) for authentication. We will cover everything from setting up the project to securing routes with JWTs.

Project Setup

1.Initialize the Project

Create a new directory for your project and initialize it with npm.

				
					mkdir jwt-auth-example
cd jwt-auth-example
npm init -y

				
			

2.Install Dependencies

Install the necessary dependencies:

				
					npm install express jsonwebtoken body-parser

				
			
  • express: The web framework for building our application.
  • jsonwebtoken: The package used to generate and verify JWTs.
  • body-parser: Middleware to parse incoming request bodies.

Create the Express.js Application

We will create a basic Express.js application with user authentication using JWTs.

File: app.js

				
					const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
const PORT = 3000;
const SECRET_KEY = 'your-secret-key'; // In production, store this securely

app.use(bodyParser.json());

// Simulated user data
const users = [
  { id: 1, username: 'john', password: 'password123', role: 'admin' },
  { id: 2, username: 'jane', password: 'password456', role: 'user' }
];

// Login route to authenticate user and generate JWT
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // Find the user in the database (simulated here)
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).send('Invalid credentials');
  }

  // Generate JWT
  const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, SECRET_KEY, { expiresIn: '1h' });

  res.json({ token });
});

// Middleware to authenticate JWT
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) return res.sendStatus(401);

  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

// Protected route - accessible only with valid JWT
app.get('/dashboard', authenticateToken, (req, res) => {
  res.json({ message: `Welcome, ${req.user.username}`, role: req.user.role });
});

// Role-based authorization middleware
function authorizeRole(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).send('Access denied: insufficient privileges');
    }
    next();
  };
}

// Admin-only route
app.get('/admin', authenticateToken, authorizeRole('admin'), (req, res) => {
  res.json({ message: 'Welcome to the admin area' });
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

				
			

Explanation

Dependencies and Configuration

  • express, jsonwebtoken, and body-parser are required at the beginning of the file.
  • SECRET_KEY is defined for signing the JWTs. In a production environment, this should be stored securely (e.g., in environment variables).

User Data

  • We have a hard-coded list of users with username, password, and role. This simulates a simple database. In a real-world scenario, you’d query an actual database.

Login Route

  • The /login route allows users to log in by sending their credentials (username and password).
  • If the credentials are valid, a JWT is generated and returned to the client.
  • The JWT includes the user’s id, username, and role, and it expires in 1 hour.

JWT Authentication Middleware

  • The authenticateToken middleware extracts the JWT from the Authorization header, verifies it, and attaches the decoded user information to req.user.
  • If the token is missing or invalid, the request is denied.

Protected Route: Dashboard

  • The /dashboard route is protected by the authenticateToken middleware. Only authenticated users can access this route.
  • If the JWT is valid, the server responds with a message and the user’s role.

Role-Based Authorization

  • The authorizeRole middleware restricts access to certain routes based on the user’s role.
  • The /admin route is accessible only to users with the admin role.

Running the Application

1.Start the Server

Run the application with:

				
					node app.js

				
			

You should see the output:

				
					Server running on http://localhost:3000

				
			

2.Testing the Application

  • Login Request: Use a tool like Postman or curl to send a POST request to /login.

				
					curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"username": "john", "password": "password123"}'

				
			
				
					// Reponse 
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJqb2huIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNjA5OTAzOTc3LCJleHAiOjE2MDk5MDc1Nzd9.YVve-sQG_6Y4-yvqHyMVJHBu5T1FbYzMXX5M5z0He6I"
}

				
			
  • Access Protected Route: Use the token in the Authorization header to access the /dashboard route.
				
					curl http://localhost:3000/dashboard -H "Authorization: Bearer <your-jwt-token>"

				
			
				
					// Response 
{
  "message": "Welcome, john",
  "role": "admin"
}

				
			
  • Access Admin Route: Use the same token to access the /admin route.
				
					curl http://localhost:3000/admin -H "Authorization: Bearer <your-jwt-token>"

				
			
				
					// Response 
{
  "message": "Welcome to the admin area"
}

				
			
  • Access with Wrong Role: If you log in as jane, who has the user role, and try to access the /admin route, you’ll get:
				
					curl http://localhost:3000/admin -H "Authorization: Bearer <jane-jwt-token>"

				
			
				
					// Response 
Access denied: insufficient privileges

				
			

JSON Web Tokens (JWT) provide a secure and efficient way to handle authentication and authorization in Express.js applications. By understanding the structure, generation, and verification of JWTs, as well as implementing advanced features like refresh tokens and blacklisting, you can create robust security mechanisms in your web applications.Happy coding !❤️

Table of Contents