nt (PDP) and Policy Enforcement Point (PEP) pattern, aligned with OIDC/OAuth2 standards.
Step 1: Identity Federation & Token Issuance
Delegate authentication to a certified OIDC provider. The provider issues an ID token (identity claims) and an access token (authorization claims). Never handle password hashing or MFA orchestration directly unless compliance mandates on-premise control.
Validate tokens at the edge or service gateway. Use cryptographic verification, check expiration, issuer, audience, and signature. Extract claims for policy evaluation.
import { jwtVerify, importJWK } from 'jose';
export async function validateAccessToken(token: string, jwksUri: string) {
const response = await fetch(jwksUri);
const jwks = await response.json();
const { payload, protectedHeader } = await jwtVerify(token, async (header) => {
const key = jwks.keys.find((k: any) => k.kid === header.kid);
if (!key) throw new Error('Unknown key ID');
return importJWK(key, header.alg);
});
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) throw new Error('Token expired');
if (!payload.iss || !payload.aud) throw new Error('Missing issuer or audience');
return payload;
}
Step 3: Policy Evaluation (ABAC/RBAC Hybrid)
Decouple policy logic from enforcement. Use a policy engine like Casbin or Open Policy Agent. Define policies as code, version them, and evaluate them against request context.
import { newEnforcer } from 'casbin';
let enforcer: any;
export async function initPolicyEngine(modelPath: string, policyPath: string) {
enforcer = await newEnforcer(modelPath, policyPath);
}
export async function evaluateAccess(subject: string, resource: string, action: string) {
if (!enforcer) throw new Error('Policy engine not initialized');
const allowed = await enforcer.enforce(subject, resource, action);
return allowed;
}
Step 4: Session & Token Lifecycle Management
Enforce short-lived access tokens (5-15 minutes) with rotating refresh tokens. Bind sessions to device fingerprint, IP range, or mTLS certificate when high assurance is required. Implement token revocation via a lightweight state store or provider-side introspection.
import { createHash } from 'crypto';
export function generateSessionBinding(deviceFingerprint: string, ip: string): string {
const data = `${deviceFingerprint}:${ip}:${Date.now()}`;
return createHash('sha256').update(data).digest('hex');
}
export async function revokeRefreshToken(tokenId: string, redis: any) {
await redis.set(`revoked:rt:${tokenId}`, '1', 'EX', 86400); // 24h TTL matches max RT lifespan
}
Step 5: Audit & Telemetry Integration
Log every policy decision, token issuance, and revocation event. Structure logs for SIEM ingestion. Include subject, resource, action, decision, context signals, and policy version.
export function logAccessDecision(decision: {
subject: string;
resource: string;
action: string;
allowed: boolean;
context: Record<string, unknown>;
policyVersion: string;
}) {
const entry = {
timestamp: new Date().toISOString(),
event: 'iam.policy_evaluation',
...decision,
};
// Forward to structured logger / SIEM pipeline
console.info(JSON.stringify(entry));
}
Architecture Decisions & Rationale
- OIDC/OAuth2 over custom auth: Standardized flows reduce implementation surface area and leverage provider-side security updates.
- Stateless token validation at PEP: Avoids round-trips to auth service for every request. Validation is cryptographic and fast.
- Policy-as-code: Versioned policies enable rollback, code review, and CI/CD integration. Prevents configuration drift.
- Short-lived access tokens + rotating refresh: Limits credential exposure window. Rotation invalidates stolen tokens without full session termination.
- Context binding: Ties sessions to device/network signals, reducing replay and session hijacking risk.
- Centralized audit pipeline: Ensures compliance readiness and enables anomaly detection without retrofitting logging.
Pitfall Guide
1. Storing JWTs in localStorage
LocalStorage is accessible to all scripts on the origin. XSS payloads can exfiltrate tokens silently. Use HttpOnly cookies for web clients or secure enclave storage for mobile/native apps. Always pair with CSRF protection if using cookies.
2. Ignoring Token Revocation
Tokens are stateless by design, but revocation is mandatory for compromised credentials or role changes. Maintain a lightweight revocation list or use provider introspection endpoints. Never rely solely on expiration for access control.
3. Over-Permissive Default Roles
Creating users with admin or editor by default guarantees privilege creep. Implement least-privilege onboarding, require explicit approval for elevated roles, and enforce periodic access reviews.
4. Hardcoding Permissions in Business Logic
Embedding if (user.role === 'admin') inside service code creates maintenance debt and inconsistent enforcement. Route all access decisions through a centralized PDP. Keep business logic focused on domain operations.
5. Skipping Device/Context Binding
Tokens alone cannot verify session legitimacy. Bind sessions to device fingerprints, IP ranges, or mTLS certificates. Invalidate sessions when context drift exceeds thresholds.
Open redirect URIs enable token interception. Strictly whitelist redirect domains and validate state parameters. CORS misconfigurations expose auth endpoints to cross-origin attacks. Restrict origins and avoid Access-Control-Allow-Origin: * on auth routes.
7. Treating Audit Logs as Afterthoughts
Logging decisions after incidents provides forensic value but zero prevention. Instrument policy evaluation, token lifecycle, and session binding events in real-time. Structure logs for automated anomaly detection and compliance reporting.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / Rapid iteration | Cloud IAM (Auth0/Okta) + Managed PEP | Reduces engineering overhead, handles compliance, scales automatically | Higher SaaS cost, lower dev time |
| Regulated enterprise / Data sovereignty | Self-hosted IAM (Keycloak/ORY) + OPA | Full control over data residency, customizable policy engine, audit transparency | Higher infra cost, requires dedicated IAM team |
| Microservices / High throughput | Custom OIDC client + Casbin/OPA + Redis revocation | Low latency, policy versioning, stateless validation, granular ABAC | Moderate dev investment, scales horizontally |
| Legacy monolith migration | Phased PDP integration + Token introspection gateway | Avoids rewrite, enforces consistent access control, enables gradual decoupling | Medium cost, reduces long-term tech debt |
Configuration Template
# casbin-model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
# casbin-policy.csv
p, admin, /api/v1/*, (GET|POST|PUT|DELETE), allow
p, editor, /api/v1/documents/*, (GET|PUT), allow
p, viewer, /api/v1/documents/*, GET, allow
g, alice, admin
g, bob, editor
g, charlie, viewer
// iam-middleware.ts
import { Request, Response, NextFunction } from 'express';
import { validateAccessToken } from './token-validator';
import { evaluateAccess, initPolicyEngine } from './policy-engine';
import { logAccessDecision } from './audit-logger';
export function iamMiddleware(policyModel: string, policyData: string) {
initPolicyEngine(policyModel, policyData);
return async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.split(' ')[1];
const claims = await validateAccessToken(token, process.env.JWKS_URI!);
const resource = req.path;
const action = req.method;
const subject = claims.sub as string;
const allowed = await evaluateAccess(subject, resource, action);
logAccessDecision({
subject,
resource,
action,
allowed,
context: { ip: req.ip, userAgent: req.headers['user-agent'] },
policyVersion: process.env.POLICY_VERSION || 'v1',
});
if (!allowed) {
return res.status(403).json({ error: 'Access denied by policy' });
}
req.user = claims;
next();
} catch (err) {
console.error('IAM middleware error:', err);
res.status(401).json({ error: 'Authentication failed' });
}
};
}
Quick Start Guide
- Provision an OIDC Provider: Create an application in Auth0, Okta, or Keycloak. Configure redirect URIs, enable MFA, and note the JWKS endpoint and issuer URL.
- Install Dependencies: Run
npm install express jose casbin. Initialize the Casbin model and policy files using the templates above.
- Attach Middleware: Import
iamMiddleware into your Express/Fastify app. Apply it to protected routes. Set environment variables for JWKS_URI, POLICY_VERSION, and provider client credentials.
- Validate Flow: Request an access token via the provider’s OAuth2 flow. Send a request to a protected endpoint with the
Authorization: Bearer <token> header. Verify policy evaluation logs and access decisions.
- Enforce Lifecycle Controls: Configure token expiration to 10 minutes. Implement refresh token rotation on your client. Add a Redis-backed revocation check to the validation pipeline. Deploy policy updates via CI/CD and verify audit stream ingestion.