Express Integration
Fire Shield provides middleware for Express applications to protect routes with RBAC.
Installation
npm install @fire-shield/express@3.1.1 @fire-shield/core@3.1.1 expressSetup
Basic Setup
import express from 'express'
import { RBAC } from '@fire-shield/core'
import { ExpressRBACAdapter } from '@fire-shield/express'
const app = express()
const rbac = new RBAC()
// Define roles
rbac.createRole('admin', ['posts:*', 'users:*'])
rbac.createRole('editor', ['posts:read', 'posts:write'])
rbac.createRole('viewer', ['posts:read'])
// Create RBAC adapter
const rbacMiddleware = new ExpressRBACAdapter(rbac, {
getUser: (req) => req.user // Get user from request
})Middleware
requirePermission
Protect routes with a permission requirement:
import { requirePermission } from '@fire-shield/express'
// Require single permission
app.post('/posts',
requirePermission('posts:write', { rbac }),
(req, res) => {
res.json({ message: 'Post created' })
}
)
app.delete('/posts/:id',
requirePermission('posts:delete', { rbac }),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)adapter.permission
Use the adapter instance to check a permission (picks up shared options automatically):
app.post('/posts',
rbacMiddleware.permission('posts:write'),
(req, res) => {
res.json({ message: 'Post created' })
}
)adapter.any / adapter.all
Require any or all of a set of permissions:
// Require at least one permission
app.get('/admin',
rbacMiddleware.any('admin:access', 'moderator:access'),
(req, res) => {
res.json({ message: 'Admin panel' })
}
)
// Require all permissions
app.delete('/posts/:id',
rbacMiddleware.all('posts:delete', 'posts:own'),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)requireRole
Protect routes with role requirements:
import { requireRole } from '@fire-shield/express'
// Require single role
app.get('/admin/users',
requireRole('admin', { rbac }),
(req, res) => {
res.json({ users: [] })
}
)adapter.role
app.get('/admin/users',
rbacMiddleware.role('admin'),
(req, res) => {
res.json({ users: [] })
}
)requireResourceAction
Check permissions by resource and action:
import { requireResourceAction } from '@fire-shield/express'
app.delete('/users/:id',
requireResourceAction('users', 'delete', { rbac }),
(req, res) => {
res.json({ message: 'User deleted' })
}
)denyPermission
Middleware that explicitly denies a permission for the request user (stores the denial in RBAC):
import { denyPermission } from '@fire-shield/express'
// Deny a permission for the user making this request
app.post('/restrict',
denyPermission('posts:delete', { rbac }),
(req, res) => {
res.json({ message: 'posts:delete has been denied for your account' })
}
)allowPermission
Middleware that removes an explicit denial for a permission:
import { allowPermission } from '@fire-shield/express'
// Remove a previously denied permission
app.post('/restore',
allowPermission('posts:delete', { rbac }),
(req, res) => {
res.json({ message: 'posts:delete has been restored' })
}
)requireNotDenied
Middleware that blocks the request if the permission is explicitly denied for the user:
import { requireNotDenied } from '@fire-shield/express'
// Block if permission is in the deny list (even if role grants it)
app.delete('/posts/:id',
requireNotDenied('posts:delete', { rbac }),
rbacMiddleware.permission('posts:delete'),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)Custom Error Handling
Default Behavior
By default, Fire Shield returns 403 Forbidden when permission is denied:
// Default response for denied access
{
error: 'Forbidden',
message: 'Insufficient permissions'
}Custom Error Handler
Customize the error response:
const rbacMiddleware = new ExpressRBACAdapter(rbac, {
getUser: (req) => req.user,
onUnauthorized: (result, req, res, next) => {
res.status(403).json({
error: 'Access Denied',
message: result.reason,
requiredPermission: result.reason
})
}
})Error Middleware
Use Express error middleware:
app.use((err, req, res, next) => {
if (err.name === 'RBACError') {
return res.status(403).json({
error: 'RBAC Error',
message: err.message,
permission: err.permission
})
}
next(err)
})Authentication Integration
With Passport.js
import passport from 'passport'
app.use(passport.initialize())
app.post('/posts',
passport.authenticate('jwt', { session: false }),
rbacMiddleware.permission('posts:write'),
(req, res) => {
// req.user is set by passport
res.json({ message: 'Post created' })
}
)With Express Session
import session from 'express-session'
app.use(session({
secret: 'your-secret',
resave: false,
saveUninitialized: true
}))
// Attach user to request from session
app.use((req, res, next) => {
if (req.session.userId) {
req.user = getUserById(req.session.userId)
}
next()
})
// Use RBAC middleware
app.post('/posts',
rbacMiddleware.permission('posts:write'),
(req, res) => {
res.json({ message: 'Post created' })
}
)With JWT
import jwt from 'jsonwebtoken'
// JWT middleware
app.use((req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (token) {
try {
req.user = jwt.verify(token, 'secret')
} catch (err) {
// Invalid token
}
}
next()
})
// RBAC middleware
app.delete('/posts/:id',
rbacMiddleware.permission('posts:delete'),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)Programmatic Permission Checks
Access the authorization result in route handlers via req.authorization:
app.get('/posts/:id', (req, res) => {
const post = getPost(req.params.id)
// req.authorization is set by the RBAC middleware after a successful check
const authorized = req.authorization?.allowed
res.json(post)
})Dynamic Permissions
Check permissions based on resource ownership:
import { requirePermission } from '@fire-shield/express'
app.delete('/posts/:id',
requirePermission('posts:delete', { rbac }),
async (req, res) => {
const post = await getPost(req.params.id)
// Check if user owns the post
const isOwner = post.authorId === req.user.id
if (!isOwner) {
return res.status(403).json({ error: 'Forbidden' })
}
await deletePost(req.params.id)
res.json({ message: 'Post deleted' })
}
)Route Organization
Grouped Routes
const adminRouter = express.Router()
// All admin routes require admin role
adminRouter.use(rbacMiddleware.role('admin'))
adminRouter.get('/users', (req, res) => {
res.json({ users: [] })
})
adminRouter.post('/users', (req, res) => {
res.json({ message: 'User created' })
})
app.use('/admin', adminRouter)Nested Permissions
const postsRouter = express.Router()
// All routes require read permission
postsRouter.use(rbacMiddleware.permission('posts:read'))
postsRouter.get('/', (req, res) => {
res.json({ posts: [] })
})
// Additional permission for write
postsRouter.post('/',
rbacMiddleware.permission('posts:write'),
(req, res) => {
res.json({ message: 'Post created' })
}
)
// Additional permission for delete
postsRouter.delete('/:id',
rbacMiddleware.permission('posts:delete'),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)
app.use('/posts', postsRouter)TypeScript Support
Full TypeScript support with type definitions:
import { Request, Response, NextFunction } from 'express'
import { RBACUser, AuthorizationResult } from '@fire-shield/core'
// Express Request is already extended by @fire-shield/express:
// req.user?: RBACUser
// req.authorization?: AuthorizationResult
// Type-safe route handler
app.get('/posts', (req: Request, res: Response) => {
if (req.authorization?.allowed) {
// Access granted
}
})Best Practices
1. Protect All Sensitive Routes
// ✅ Good: Protected routes
app.post('/posts',
rbacMiddleware.permission('posts:write'),
createPost
)
app.delete('/posts/:id',
rbacMiddleware.permission('posts:delete'),
deletePost
)
// ❌ Avoid: Unprotected sensitive routes
app.delete('/posts/:id', deletePost)2. Use Router-Level Middleware
// ✅ Good: Protect entire router
const adminRouter = express.Router()
adminRouter.use(rbacMiddleware.role('admin'))
adminRouter.get('/users', getUsers)
adminRouter.post('/users', createUser)
// ❌ Avoid: Repeat on every route
app.get('/admin/users',
rbacMiddleware.role('admin'),
getUsers
)
app.post('/admin/users',
rbacMiddleware.role('admin'),
createUser
)3. Handle Errors Gracefully
// ✅ Good: Custom error handling
const rbacMiddleware = new ExpressRBACAdapter(rbac, {
getUser: (req) => req.user,
onUnauthorized: (result, req, res, next) => {
console.warn(`Access denied to ${req.path}`, {
user: req.user?.id,
reason: result.reason
})
res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions'
})
}
})4. Document Permission Requirements
/**
* Create a new post
* @route POST /posts
* @permission posts:write
* @returns {Post} Created post
*/
app.post('/posts',
rbacMiddleware.permission('posts:write'),
createPost
)Complete Example
import express from 'express'
import { RBAC } from '@fire-shield/core'
import { ExpressRBACAdapter } from '@fire-shield/express'
const app = express()
const rbac = new RBAC()
// Setup roles
rbac.createRole('admin', ['*'])
rbac.createRole('editor', ['posts:*', 'comments:*'])
rbac.createRole('viewer', ['posts:read', 'comments:read'])
// RBAC adapter
const rbacMiddleware = new ExpressRBACAdapter(rbac, {
getUser: (req) => req.user,
onUnauthorized: (result, req, res, next) => {
res.status(403).json({
error: 'Forbidden',
message: result.reason || 'Insufficient permissions'
})
}
})
app.use(express.json())
// Public routes
app.get('/health', (req, res) => {
res.json({ status: 'ok' })
})
// Protected routes
app.get('/posts',
rbacMiddleware.permission('posts:read'),
(req, res) => {
res.json({ posts: [] })
}
)
app.post('/posts',
rbacMiddleware.permission('posts:write'),
(req, res) => {
res.json({ message: 'Post created' })
}
)
app.delete('/posts/:id',
rbacMiddleware.permission('posts:delete'),
(req, res) => {
res.json({ message: 'Post deleted' })
}
)
// Admin routes
const adminRouter = express.Router()
adminRouter.use(rbacMiddleware.role('admin'))
adminRouter.get('/users', (req, res) => {
res.json({ users: [] })
})
adminRouter.post('/users', (req, res) => {
res.json({ message: 'User created' })
})
app.use('/admin', adminRouter)
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})Next Steps
- Explore Fastify Integration
- Learn about Permissions
- Check out API Reference
