Express JWT Permissions Middleware
raw JSON →Express JWT Permissions is an authorization middleware for Node.js applications, designed to work in conjunction with JWT authentication solutions like `express-jwt`. It inspects a decoded JWT token, typically found on `req.user` (or a configurable property), for a permissions array or a space-delimited scope string. The library, currently at stable version 1.3.7, has a moderate release cadence, primarily focusing on security updates, dependency bumps, and TypeScript typing enhancements. Its key differentiator lies in its flexible permission checking logic, supporting simple strings, arrays for AND logic, and nested arrays for complex OR logic combinations of permissions. It also provides configurable options for `requestProperty` and `permissionsProperty` to accommodate diverse JWT payload structures, moving beyond the default `req.user.permissions` pattern, and facilitates custom error handling for permission denials.
Common errors
error UnhandledPromiseRejectionWarning: Error: permission_denied ↓
err.code === 'permission_denied' and respond with an appropriate status (e.g., 403 Forbidden). error TypeError: Cannot read properties of undefined (reading 'permissions') ↓
express-jwt-permissions. Ensure guardFactory is configured with requestProperty and permissionsProperty to match the actual location of permissions in your decoded JWT payload. error TypeError: guard.check is not a function ↓
const guardFactory = require('express-jwt-permissions'); const guard = guardFactory(); or import guardFactory from 'express-jwt-permissions'; const guard = guardFactory(); error Property 'permissions' does not exist on type 'Request<ParamsDictionary, any, any, Query, Record<string, any>>'. ↓
express Request type in a declaration file (e.g., src/types/express.d.ts): declare namespace Express { interface Request { user?: { permissions?: string[] | string; [key: string]: any; }; auth?: { scope?: string | string[]; [key: string]: any; }; } }. Adjust user to your requestProperty and permissions to your permissionsProperty. Warnings
breaking Beginning with v1.3.7, `express-jwt-permissions` upgraded its internal `express-unless` dependency to v2. While the primary impact was internal typings, users indirectly relying on `express-unless`'s API or sensitive to transitive major version bumps should review the `express-unless` v2 changelog for potential breaking changes in their specific use cases. ↓
gotcha `express-jwt-permissions` *must* be used after a JWT authentication middleware (e.g., `express-jwt`) that successfully decodes the token and attaches the payload to `req.user` (or a configured `requestProperty`). Without a decoded token on the request object, permission checks will always fail. ↓
gotcha By default, the middleware expects permissions to be an array or string at `req.user.permissions`. If your decoded JWT token stores permissions in a different location (e.g., `req.auth.scope` or `req.identity.roles`), you *must* configure `requestProperty` and `permissionsProperty` when initializing the guard. ↓
gotcha When a permission check fails, `express-jwt-permissions` throws an error with `err.code === 'permission_denied'`. If this error is not explicitly caught and handled by a custom Express error middleware, it can lead to an unhandled error, a generic 500 status, or unintended application behavior. ↓
deprecated Prior to v1.3.2, TypeScript projects with `esModuleInterop: false` in `tsconfig.json` might have encountered issues with typings for `express-jwt-permissions` due to how default imports were handled, potentially leading to compilation errors or incorrect type inference. ↓
Install
npm install express-jwt-permissions yarn add express-jwt-permissions pnpm add express-jwt-permissions Imports
- guardFactory wrong
import { guardFactory } from 'express-jwt-permissions';correctimport guardFactory from 'express-jwt-permissions'; - GuardOptions wrong
import GuardOptions from 'express-jwt-permissions';correctimport { GuardOptions } from 'express-jwt-permissions'; - require wrong
const { guardFactory } = require('express-jwt-permissions');correctconst guardFactory = require('express-jwt-permissions');
Quickstart
import express from 'express';
import { expressjwt as jwt } from 'express-jwt';
import guardFactory, { GuardOptions } from 'express-jwt-permissions';
const app = express();
const PORT = 3000;
// IMPORTANT: Replace 'YOUR_JWT_SECRET' with a strong secret from environment variables
// This mock JWT middleware simulates express-jwt's behavior.
const mockJwtMiddleware = jwt({
secret: process.env.JWT_SECRET || 'supersecretjwtkeythatshouldbemorecomplex',
algorithms: ['HS256'],
requestProperty: 'auth', // Where the decoded JWT payload will be placed (e.g., req.auth)
getToken: (req) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
return null;
}
}).unless({ path: ['/public'] }); // Example: allow /public without a token
app.use(mockJwtMiddleware);
// Initialize the permission guard, configuring it to look for permissions
// within the 'auth' property on the request object and specifically in a 'scope' field.
const guard = guardFactory({
requestProperty: 'auth',
permissionsProperty: 'scope'
} as GuardOptions);
// Public route, accessible without any specific permissions
app.get('/public', (req, res) => {
res.send('Welcome to the public area!');
});
// Route requiring 'user:read' permission
app.get('/user/profile', guard.check('user:read'), (req, res) => {
res.send(`User profile for ${req.auth?.sub || 'unknown'}. Access granted with user:read.`);
});
// Route requiring 'admin' OR ('user:write' AND 'user:delete') permissions
app.post('/admin/manage', guard.check([
['admin'],
['user:write', 'user:delete']
]), (req, res) => {
res.send(`Admin management area. User ${req.auth?.sub || 'unknown'} has required permissions.`);
});
// Global error handler for permission_denied errors
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err.code === 'permission_denied') {
console.error('Permission denied:', err.message);
return res.status(403).send('Forbidden: Insufficient permissions.');
}
next(err); // Pass other errors to the next error handler
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log('Test with valid JWTs in Authorization: Bearer <token>');
console.log('e.g., token with payload { "sub": "user1", "scope": "user:read" }');
console.log('e.g., token with payload { "sub": "admin1", "scope": "admin user:write user:delete" }');
});