Protecting Routes with Cognito JWT Middleware
Once a user logs in, they receive a Cognito Access Token — a JWT signed by your User Pool. To protect Express routes, you need middleware that verifies this token on every request.
How JWT Verification Works
Cognito signs tokens using RSA private keys. It publishes the corresponding public keys (JWKS) at a well-known URL:
https://cognito-idp.<region>.amazonaws.com/<UserPoolId>/.well-known/jwks.json
The aws-jwt-verify library:
- Fetches and caches those public keys on first use
- On every request, verifies the token's signature and claims locally — no network call to Cognito
- Throws if the token is expired, tampered with, or issued by a different User Pool
This is the correct approach. Never call Cognito's API to validate a token on every request — it's slow and has rate limits.
Install
- npm
- Yarn
- pnpm
- Bun
npm install aws-jwt-verify
yarn add aws-jwt-verify
pnpm add aws-jwt-verify
bun add aws-jwt-verify
Create the Verifier
import { CognitoJwtVerifier } from "aws-jwt-verify";
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID,
tokenUse: "access", // Verify Access Tokens (use "id" for ID Tokens)
clientId: process.env.COGNITO_CLIENT_ID,
});
export default verifier;
Auth Middleware
import verifier from "../config/jwtVerifier.js";
export async function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Authorization header missing or malformed" });
}
const token = authHeader.split(" ")[1];
try {
const payload = await verifier.verify(token);
req.user = payload; // Attach decoded claims to the request
next();
} catch (err) {
return res.status(401).json({ message: "Invalid or expired token" });
}
}
The verified payload contains claims like:
{
"sub": "user-uuid-from-cognito",
"username": "rizwan@example.com",
"email": "rizwan@example.com",
"cognito:groups": ["admin"],
"exp": 1712345678,
"iat": 1712342078
}
Role-Based Authorization Middleware
Cognito lets you add users to Groups (e.g., admin, moderator). Use this to implement role-based access control:
export function authorize(...allowedRoles) {
return (req, res, next) => {
const userGroups = req.user?.["cognito:groups"] || [];
const hasRole = allowedRoles.some((role) => userGroups.includes(role));
if (!hasRole) {
return res.status(403).json({ message: "You do not have permission to access this resource" });
}
next();
};
}
Applying the Middleware to Routes
import express from "express";
import { authenticate } from "../middlewares/authenticate.js";
import { authorize } from "../middlewares/authorize.js";
const router = express.Router();
// Public — no auth needed
router.get("/", async (req, res) => {
res.json({ products: [] });
});
// Protected — any authenticated user
router.get("/my-orders", authenticate, async (req, res) => {
// req.user is available here
const userId = req.user.sub;
res.json({ userId, orders: [] });
});
// Admin only
router.delete("/:id", authenticate, authorize("admin"), async (req, res) => {
res.json({ message: `Product ${req.params.id} deleted` });
});
// Multiple allowed roles
router.patch("/:id", authenticate, authorize("admin", "moderator"), async (req, res) => {
res.json({ message: `Product ${req.params.id} updated` });
});
export default router;
Apply Auth Globally (Optional)
If most of your routes are protected, apply authenticate globally and mark public routes explicitly:
import express from "express";
import { authenticate } from "./middlewares/authenticate.js";
import authRouter from "./routes/auth.js";
import productRouter from "./routes/products.js";
const app = express();
app.use(express.json());
// Auth routes are public — register them before the global middleware
app.use("/auth", authRouter);
// All routes below this line require a valid Cognito token
app.use(authenticate);
app.use("/products", productRouter);
app.listen(3000);
Accessing User Info in Route Handlers
After authenticate runs, req.user holds all the token's claims:
router.get("/profile", authenticate, async (req, res) => {
res.json({
id: req.user.sub, // Cognito user UUID
email: req.user.email,
username: req.user.username,
groups: req.user["cognito:groups"] || [],
});
});
Testing Protected Routes with Postman
- First call
POST /auth/loginto get anaccessToken - In the protected request, go to Authorization → Bearer Token
- Paste the
accessToken - Send the request — the middleware verifies it automatically
Key Takeaways
- Verify Cognito JWTs locally using cached public keys — never call Cognito's API per request
aws-jwt-verifyhandles key caching, signature verification, and claim validation automatically- Always attach the verified payload to
req.userso downstream handlers can read user context - Use Cognito Groups +
authorize()middleware for role-based access control - Register public routes (like
/auth) before any globalauthenticatemiddleware