Securing API: Restrict Access to Admins Only in ExpressJS
API security ensures data protection by restricting sensitive operations to admins and validating user sessions. Middleware like allowAdminsOnly enforces admin-only access, while authorise verifies tokens and trusted devices for robust security across web and mobile clients.

API security is a critical aspect of modern web applications. Restricting sensitive operations to authorized users, such as administrators, ensures data protection and system integrity. This blog will demonstrate how to secure your API by implementing middleware to allow only admins and an authorization mechanism to validate user sessions and trusted devices.
Middleware for Admin-Only Access
The allowAdminsOnly
middleware ensures that only users with an admin role can access specific routes. Here’s the implementation:
import { Request, Response, NextFunction } from "express";
import { ExtendedRequest } from "../domains/user/types";
import { userTypes } from "../utils/constants";
export const allowAdminsOnly = (req: Request, res: Response, next: NextFunction) => {
try {
const user = (req as ExtendedRequest).user;
if (!user) {
return res.status(401).json({ error: "Unauthorized. Please log in." });
}
if (user.user_role === userTypes.ADMIN) {
return next();
}
return res.status(403).json({ error: "You are not an admin" });
} catch (error) {
console.error("Error in checking admin role:", error);
res.status(500).json({ error: "Internal server error" });
}
};
Key Features
- Checks if the user is logged in.
- Verifies if the logged-in user has the
ADMIN
role. - Returns appropriate HTTP status codes for unauthorized or forbidden access.
Authorization Middleware
The authorise
middleware validates user sessions and ensures that requests are made from trusted devices. Here’s the implementation:
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import { ExtendedRequest } from "../domains/user/types";
import TrustedDeviceModel from "../domains/trusted_devices/model";
export const authorise = (req: Request, res: Response, next: NextFunction) => {
try {
let accessToken;
let deviceId;
const isMobileApp = req.headers["client-type"] === "mobile_app";
if (isMobileApp) {
accessToken = req.headers.authorization?.split(" ")[1];
deviceId = req.headers.deviceId;
} else {
accessToken = req.cookies?.accessToken;
deviceId = req.cookies?.deviceId;
}
if (!accessToken) {
return res.status(401).json({ error: "User not logged in" });
}
jwt.verify(accessToken, process.env.SECRET_KEY || "", async (err: any, decoded: any) => {
if (err) {
return res.status(403).json({ error: "Invalid access token" });
}
const device = await TrustedDeviceModel.findOne({
user_id: decoded.userId,
device_id: deviceId,
isActive: true,
});
if (!device) {
return res.status(401).json({ error: "Invalid or inactive device. Please Login Again" });
}
(req as ExtendedRequest).user = decoded;
next();
});
} catch (error) {
console.error("Error in authorise middleware:", error);
res.status(500).json({ error: "Internal server error" });
}
};
Key Features
- Supports both mobile app and web clients.
- Verifies the access token and ensures it is valid.
- Checks the device’s validity using a trusted device database.
To implement these middlewares, we’ll use a streamlined user model. Here’s a cleaned-up version of the user schema:
import { Schema, model } from "mongoose";
import { UserDocument, UserModel } from "./types";
const userSchema = new Schema({
username: {
type: String,
unique: true,
required: false,
index: true,
},
full_name: {
type: String,
required: true,
},
email_address: {
type: String,
unique: true,
required: true,
index: true,
},
user_role: {
type: String,
required: true,
enum: ["admin", "user"],
default: "user",
index: true,
},
});
const UserModel = model<UserDocument, UserModel>("User", userSchema);
export default UserModel;
Key Fields
username
,full_name
, andemail_address
: Basic user details.user_role
: Differentiates between admin and regular users.
Using the Middlewares
To protect your routes, simply apply the middleware:
import express from "express";
import { allowAdminsOnly } from "./middlewares/allowAdminsOnly";
import { authorise } from "./middlewares/authorise";
const app = express();
// Route accessible only by admins
app.get("/admin", authorise, allowAdminsOnly, (req, res) => {
res.send("Welcome, Admin!");
});
// Public route
app.get("/public", (req, res) => {
res.send("Welcome, Guest!");
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});
By implementing the allowAdminsOnly
and authorise
middlewares, you can effectively secure your API. The admin-only restriction ensures sensitive operations are safeguarded, while the authorization process validates users and devices, adding another layer of security.
Integrate these techniques into your Node.js application to protect your resources and maintain robust API security.
That's it for today. See ya 👋