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.
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:
A JWT is structured as follows:
header.payload.signature
The process of using JWT typically involves the following steps:
Authorization
header of subsequent requests to access protected routes.The header typically consists of two parts:
{
"alg": "HS256",
"typ": "JWT"
}
The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims:
iss
(issuer), exp
(expiration time), sub
(subject), and aud
(audience).
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
To create the signature, you need to take the encoded header, the encoded payload, a secret, and the algorithm specified in the header.
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The resulting JWT looks something like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
To demonstrate JWT implementation in an Express.js application, we’ll start by setting up a simple project.
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());
jsonwebtoken
package for handling JWTs.When a user logs in, the server will authenticate the user and generate a JWT.
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 });
});
/login
. If the credentials are valid, we generate a JWT using jwt.sign()
and send it back to the client.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
After logging in and receiving a JWT, the client will include this token in the Authorization
header of requests to access protected routes.
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 });
});
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./dashboard
route is protected by the authenticateToken
middleware. Only requests with a valid JWT can access this route./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.
You can extend the JWT implementation to enforce role-based access control (RBAC) by checking the user’s role.
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' });
});
/admin
route is only accessible to users with the ‘admin’ role./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
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.
Refresh tokens are usually stored securely on the client side (e.g., in a cookie or local storage).
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);
});
/token
route generates a new access token using a valid refresh token./logout
route invalidates the refresh token by removing it from the stored list./token
with a valid refresh token returns a new access token./logout
with a valid refresh token removes it from the list, preventing future token refreshes.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.
You can store blacklisted tokens in a database or an in-memory store.
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);
/blacklist
route adds the token to the blacklist./dashboard
or /admin
with a blacklisted token returns a 403 status.Short expiration times reduce the risk of a compromised token being used maliciously. Use refresh tokens to extend user sessions securely.
Store refresh tokens securely, preferably in an HTTP-only cookie. Avoid storing them in local storage to prevent XSS attacks.
Always use HTTPS to transmit tokens securely over the network. This prevents tokens from being intercepted by attackers.
Implement mechanisms like refresh tokens and blacklisting to revoke tokens when necessary, such as during logout or if a token is compromised.
When a token expires, inform the user and provide a seamless way to refresh the token or re-authenticate.
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.
Create a new directory for your project and initialize it with npm
.
mkdir jwt-auth-example
cd jwt-auth-example
npm init -y
Install the necessary dependencies:
npm install express jsonwebtoken body-parser
We will create a basic Express.js application with user authentication using JWTs.
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}`);
});
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).username
, password
, and role
. This simulates a simple database. In a real-world scenario, you’d query an actual database./login
route allows users to log in by sending their credentials (username and password).id
, username
, and role
, and it expires in 1 hour.authenticateToken
middleware extracts the JWT from the Authorization
header, verifies it, and attaches the decoded user information to req.user
./dashboard
route is protected by the authenticateToken
middleware. Only authenticated users can access this route.authorizeRole
middleware restricts access to certain routes based on the user’s role./admin
route is accessible only to users with the admin
role.Run the application with:
node app.js
You should see the output:
Server running on http://localhost:3000
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"
}
Authorization
header to access the /dashboard
route.
curl http://localhost:3000/dashboard -H "Authorization: Bearer "
// Response
{
"message": "Welcome, john",
"role": "admin"
}
/admin
route.
curl http://localhost:3000/admin -H "Authorization: Bearer "
// Response
{
"message": "Welcome to the admin area"
}
jane
, who has the user
role, and try to access the /admin
route, you’ll get:
curl http://localhost:3000/admin -H "Authorization: Bearer "
// 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 !❤️