Skip to content
Learni
View all tutorials
Sécurité

How to Implement ABAC in a Node.js API in 2026

Lire en français

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

terminal
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 --init

This 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

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

src/types.ts
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

src/abac.ts
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

policies.json
[
  {
    "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

src/middleware/abac.ts
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

src/server.ts
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 " -d '{"resource":{"department":"HR"}}' localhost:3000/posts/read. 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=false with context.
  • Test thoroughly: Use Mocha for edge cases (bad token, off-hours).
  • Limit sensitive attributes: Never include password in 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.resource with 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!