In our previous post, we discussed the fundamentals of authentication (AuthN) and authorization (AuthZ). We saw how to protect routes using PassportJS but noticed a gap: it doesn’t provide a built-in, flexible way to handle Role-Based Access Control (RBAC).
To solve this, we started building our own ExpressAuth class. In this post, we’ll dive deeper into the implementation, specifically focusing on the verifyToken method. This method is the "heart" of our security layer—it's responsible for extracting the Bearer token, validating it against our secret key, and returning a structured result (TokenResult) instead of just a simple boolean.
Let’s look at how we handle token extraction and verification safely:
private extractBearerToken(req: Request): string | null {
const authHeader = req.headers['authorization'];
if (!authHeader?.startsWith('Bearer ')) return null;
const token = authHeader.split(' ')[1];
return token || null;
}
private verifyToken(req: Request): TokenResult {
const token = this.extractBearerToken(req);
if (!token) {
return {
success: false,
payload: null,
statusCode: 401,
message: 'Authorization header is missing or invalid.',
};
}
try {
const payload = jwt.verify(token, this.secretKey);
return { success: true, payload };
} catch (error) {
return {
success: false,
payload: null,
statusCode: 401,
message: 'Invalid or expired token.',
};
}
}The AuthGuard is a standard Express middleware. Its job is simple but critical: intercept the incoming request, hand it over to verifyToken, and decide whether to let the request proceed to the next handler or block it right there.
Here is the implementation:
public AuthGuard(req: Request, res: Response, next: NextFunction): void {
const result = this.verifyToken(req);
if (!result.success) {
res.status(result.statusCode).send(result.message);
return;
}
(req as any).info = result.payload;
next();
}While the AuthGuard ensures we know who the user is, the RoleGuard determines what they are allowed to do. This is where we implement our authorization (AuthZ) logic.
The RoleGuard is a bit more sophisticated because it’s a higher-order function. It takes a list of allowed roles and returns a middleware function that Express can execute.
Here is the implementation:
public RoleGuard(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = this.verifyToken(req);
if (!result.success) {
res.status(result.statusCode).send(result.message);
return;
}
const payload = result.payload as jwt.JwtPayload;
const hasRole = this.roleKey && allowedRoles.includes(payload[this.roleKey]);
if (!hasRole) {
res.status(403).send('Forbidden');
return;
}
(req as any).info = payload;
next();
};
}After breaking down the logic, here is the complete class. This single file will handle token extraction, verification, and role-based authorization for your entire Express application.
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
interface AuthConfig {
secretKey: string;
roleKey: string;
}
interface TokenSuccess {
success: true;
payload: jwt.JwtPayload | string;
}
interface TokenFailure {
success: false;
payload: null;
statusCode: number;
message: string;
}
type TokenResult = TokenSuccess | TokenFailure;
export class ExpressAuth {
private secretKey: string;
private roleKey: string;
constructor(options: AuthConfig) {
this.secretKey = options.secretKey;
this.roleKey = options.roleKey;
}
public AuthGuard = (req: Request, res: Response, next: NextFunction): void => {
const result = this.verifyToken(req);
if (!result.success) {
res.status(result.statusCode).send(result.message);
return;
}
(req as any).info = result.payload;
next();
}
public RoleGuard(...allowedRoles: string[]) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = this.verifyToken(req);
if (!result.success) {
res.status(result.statusCode).send(result.message);
return;
}
const payload = result.payload as jwt.JwtPayload;
const hasRole = this.roleKey && allowedRoles.includes(payload[this.roleKey]);
if (!hasRole) {
res.status(403).send('Forbidden');
return;
}
(req as any).info = payload;
next();
};
}
private extractBearerToken(req: Request): string | null {
const authHeader = req.headers['authorization'];
if (!authHeader?.startsWith('Bearer ')) return null;
const token = authHeader.split(' ')[1];
return token || null;
}
private verifyToken(req: Request): TokenResult {
const token = this.extractBearerToken(req);
if (!token) {
return {
success: false,
payload: null,
statusCode: 401,
message: 'Authorization header is missing or invalid.',
};
}
try {
const payload = jwt.verify(token, this.secretKey);
return { success: true, payload };
} catch (error) {
return {
success: false,
payload: null,
statusCode: 401,
message: 'Invalid or expired token.',
};
}
}
}Now that we have our ExpressAuth class ready, let's see how easy it is to secure an Express application. In this example, we’ll create a simple server with three levels of access: public, authenticated, and admin-only.
const express = require('express');
const { ExpressAuth } = require('./ExpressAuth');
const app = express();
const auth = new ExpressAuth({
secretKey: 'secretKey',
roleKey: 'role'
});
app.get('/public', (req, res) => {
res.json({ message: "This is a public endpoint." });
});
app.get('/profile', auth.AuthGuard, (req, res) => {
res.json({
message: "Welcome to your profile!",
user: req.info
});
});
app.get('/admin', auth.RoleGuard('admin'), (req, res) => {
res.json({ message: "Welcome, Almighty Admin!" });
});
app.listen(3000, () => console.log('Server running on port 3000'));I hope this two-part series helped you bridge the gap between simple authentication and robust role-based authorization. Building scalable security layers is one of the most rewarding parts of backend engineering.
If you found this helpful, feel free to follow me for more backend engineering deep-dives!









Comments (1)