Introduction
ABAC (Attribute-Based Access Control) is revolutionizing authorization management in enterprises. Unlike RBAC (fixed roles), ABAC dynamically evaluates access by cross-referencing user attributes (role, department, location), resource attributes (owner, sensitivity), actions (read, write, delete), and environment factors (time, IP).
Why adopt it in 2026? Regulations like GDPR and microservices architectures demand fine-grained control. Picture an HR employee accessing files only if their department matches and it's within business hours—ABAC handles this natively.
This beginner tutorial builds a secure Node.js/Express ABAC API from scratch. You'll end up with reusable middleware, configurable JSON policies, and functional tests. Finally, your API will block unauthorized access with precise HTTP 403 responses. Ready to secure like a pro? (128 words)
Prerequisites
- Node.js 20+ installed
- Basic knowledge of TypeScript and Express
- An editor like VS Code
- 10 minutes to test locally
Initialize the project and install dependencies
mkdir abac-api && cd abac-api
npm init -y
npm install express jsonwebtoken @types/express @types/jsonwebtoken @types/node
tnpm install -D typescript ts-node nodemon @types/node
npx tsc --initThis sequence creates the project folder, initializes npm, installs Express for the server, JWT to simulate authentication, and TypeScript for static typing. The dev dependencies enable compiling and running TS directly with ts-node and nodemon for hot-reload.
Configure TypeScript
Before coding, let's tweak tsconfig.json for modern support. This enables strict mode to catch errors early, like a safety net. Run npx tsc --init if you haven't, then update it as shown below.
Complete tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}This file sets up TypeScript for ES2022, an output directory for builds, and strict mode that enforces explicit typing—preventing runtime bugs like undefined on optional props. resolveJsonModule lets you import our ABAC policies as JSON.
Define ABAC types
ABAC relies on structured attributes. We type the User (role, dept, location), Resource (owner, type), Action, and Environment. This makes the code self-documenting and IntelliSense-friendly.
Types for ABAC attributes
export interface UserAttributes {
sub: string;
role: 'admin' | 'manager' | 'employee';
department: string;
location: string;
}
export interface ResourceAttributes {
owner: string;
type: 'post' | 'document';
department: string;
sensitivity: 'public' | 'private';
}
export type Action = 'read' | 'write' | 'delete';
export interface EnvironmentAttributes {
time: string;
ip: string;
}
export interface AbacRequest {
user: UserAttributes;
resource: ResourceAttributes;
action: Action;
env: EnvironmentAttributes;
}These interfaces precisely define the attributes evaluated by ABAC. For example, UserAttributes includes department for rules like 'intra-department access'. Use them everywhere for strong typing that prevents missing prop errors.
Create the ABAC policy engine
The heart of ABAC is a rules evaluator. We load simple JSON policies (logical rules) and apply them. Think of it like a judge checking if all criteria of a law are met.
ABAC engine and JSON policies
import { AbacRequest, UserAttributes, ResourceAttributes, Action, EnvironmentAttributes } from './types';
import policies from '../policies.json';
export function evaluateAbac(req: AbacRequest): boolean {
const policy = policies.find(p =>
p.resourceType === req.resource.type &&
p.action === req.action
);
if (!policy) return false;
for (const rule of policy.rules) {
if (!rule.condition(req.user, req.resource, req.action, req.env)) {
return false;
}
}
return true;
}
export function getUserFromToken(token: string): UserAttributes | null {
try {
// Simulation JWT decode (utilisez jwt.verify en prod)
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
return {
sub: payload.sub,
role: payload.role,
department: payload.department,
location: payload.location
};
} catch {
return null;
}
}This function loads JSON rules, iterates through them, and applies custom conditions. getUserFromToken extracts user attributes from a mock JWT. Pitfall: always validate token signatures in production with jwt.verify to prevent fakes.
ABAC JSON policies file
[
{
"resourceType": "post",
"action": "read",
"rules": [
{
"condition": (user: any, resource: any) => user.role === 'admin' || user.department === resource.department
},
{
"condition": (user: any, resource: any, action: any, env: any) => new Date(env.time).getHours() >= 9 && new Date(env.time).getHours() <= 17
}
]
},
{
"resourceType": "post",
"action": "write",
"rules": [
{
"condition": (user: any, resource: any) => user.sub === resource.owner || user.role === 'admin'
}
]
}
]This JSON defines declarative ABAC rules: read if same dept or admin AND business hours; write if owner. Easy to edit without redeploying. In production, load dynamic rules from a database.
Implement the ABAC middleware
The middleware intercepts every request, extracts attributes, and calls the evaluator. It returns 403 on failure, with details for debugging.
Protective ABAC middleware
import { Request, Response, NextFunction } from 'express';
import { evaluateAbac, getUserFromToken } from '../abac';
import { ResourceAttributes, Action, EnvironmentAttributes } from '../types';
declare global {
namespace Express {
interface Request {
userAttrs?: any;
resourceAttrs?: ResourceAttributes;
action?: Action;
}
}
}
export function abacMiddleware(requiredResourceType: string, action: Action) {
return (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Token requis' });
const user = getUserFromToken(token);
if (!user) return res.status(401).json({ error: 'Token invalide' });
req.userAttrs = user;
req.action = action;
const resource: ResourceAttributes = req.body.resource || { owner: 'unknown', type: requiredResourceType as any, department: 'default', sensitivity: 'public' };
req.resourceAttrs = resource;
const env: EnvironmentAttributes = {
time: new Date().toISOString(),
ip: req.ip || 'unknown'
};
if (evaluateAbac({ user, resource, action, env })) {
next();
} else {
res.status(403).json({ error: 'Accès refusé par ABAC' });
}
};
}This factory middleware takes resourceType and action, parses the token, builds the ABAC context, and evaluates it. Extends Request for custom typed props. Pitfall: resource is mocked from req.body for demo; fetch from DB in production.
Create the server and protected routes
Let's put it all together in server.ts. Add /posts/read and /posts/write routes protected by ABAC. Run with npx nodemon src/server.ts.
Complete Express server with ABAC
import express from 'express';
import { abacMiddleware } from './middleware/abac';
const app = express();
app.use(express.json());
const PORT = 3000;
// Token mock pour tests (générez avec jwt.sign en prod)
const mockTokens = {
admin: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiZGVwYXJ0bWVudCI6IkhSIiwibG9jYXRpb24iOiJQYXJpcyJ9.dummy',
employee: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyIiwicm9sZSI6ImVtcGxveWVlIiwiZGVwYXJ0bWVudCI6IkhSIiwibG9jYXRpb24iOiJQYXJpcyJ9.dummy'
};
app.post('/posts/read', abacMiddleware('post', 'read'), (req, res) => {
res.json({ message: 'Post lu avec succès', data: { id: 1, content: 'Contenu sensible' } });
});
app.post('/posts/write', abacMiddleware('post', 'write'), (req, res) => {
const resource = req.body.resource || {};
resource.owner = (req as any).userAttrs.sub;
res.json({ message: 'Post écrit avec succès', resource });
});
app.listen(PORT, () => {
console.log(`Serveur ABAC sur http://localhost:${PORT}`);
console.log('Tokens test: admin=', mockTokens.admin);
console.log('employee=', mockTokens.employee);
});Full server with ABAC-protected routes. Mock tokens have visible payloads (HR dept). Test with curl: curl -H "Authorization: Bearer . Admin always passes, employee only if dept matches + business hours.
Best practices
- Store policies in a DB: Load JSON from PostgreSQL for dynamic updates without restarts.
- Cache evaluations: Use Redis to memoize frequent results (TTL 5min).
- Log denials: Add Winston to trace
evaluateAbac=falsewith context. - Test thoroughly: Use Mocha for edge cases (bad token, off-hours).
- Limit sensitive attributes: Never include
passwordin user attrs.
Common pitfalls to avoid
- Forgetting env attrs: Without
time, time-based rules fail silently. - No admin fallback: Always prioritize
role === 'admin'in first rule. - Unsigned JWTs: Mock fine for dev, but
jwt.verify(secret)mandatory in prod. - Missing resource: Validate
req.body.resourcewith Zod to avoid crashes.
Next steps
Master advanced ABAC with OPA (Open Policy Agent) for Kubernetes. Explore Cerbos for policies as code.
Check out our Learni backend security trainings: Node.js Expert, Advanced Authentication. Join the newsletter for 2026 tutorials!