Authentication & Authorization System Design (RBAC/ABAC)

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-23 Freshness: 2026-02-23

TL;DR

Constraints

Quick Reference

ComponentRoleTechnology OptionsScaling Strategy
Identity Provider (IdP)Authenticates users, issues tokensAuth0, Okta, Keycloak, AWS Cognito, customHorizontal behind LB; federate with OIDC
Password StorageStores hashed credentialsbcrypt, Argon2id, scryptTune cost factor to ~250ms per hash
Token ServiceIssues and refreshes access/refresh tokensJWT (RS256/ES256), opaque tokensShort-lived access (15min), long-lived refresh (7d)
API GatewayValidates tokens, rate limits, routesKong, AWS API Gateway, Envoy, NginxHorizontal; cache JWKS with TTL
Session StoreStores server-side sessions or refresh tokensRedis, Memcached, DynamoDBCluster mode with replication
RBAC EngineMaps users to roles to permissionsCasbin, OPA, custom middlewareCache role-permission matrix; invalidate on change
ABAC EngineEvaluates attribute-based policiesOPA (Rego), Cedar (AWS), XACMLSidecar per service; precompile policies
ReBAC EngineChecks relationship graphs for accessSpiceDB, Ory Keto, OpenFGADistributed graph with caching (Zanzibar model)
MFA ServiceSecond-factor verificationTOTP (Google Authenticator), WebAuthn/FIDO2, SMSStateless TOTP; WebAuthn for phishing resistance
Audit LogRecords auth events for complianceELK stack, CloudWatch, DatadogAppend-only, immutable, separate storage
Secret ManagementStores signing keys, API secretsHashiCorp Vault, AWS Secrets Manager, GCP KMSAuto-rotate keys; zero-trust access
Rate LimiterPrevents brute-force and credential stuffingRedis + sliding window, Cloudflare WAFPer-IP and per-account; adaptive thresholds

Decision Tree

START: Choose Authorization Model
├── Do users have clearly defined roles (admin, editor, viewer)?
│   ├── YES → Are there fewer than 20 roles?
│   │   ├── YES → Use RBAC (simplest, fits 80% of apps)
│   │   └── NO → Role explosion risk — consider ABAC or hybrid
│   └── NO ↓
├── Do access decisions depend on attributes (time, location, department, resource owner)?
│   ├── YES → Are policies complex (3+ attributes per decision)?
│   │   ├── YES → Use ABAC with a policy engine (OPA, Cedar)
│   │   └── NO → RBAC + attribute filters (hybrid approach)
│   └── NO ↓
├── Do access decisions depend on relationships (user owns document, user is in org)?
│   ├── YES → Use ReBAC (SpiceDB, OpenFGA) — Google Zanzibar model
│   └── NO ↓
├── Scale > 100K concurrent users?
│   ├── YES → Use dedicated authz service (SpiceDB, OPA sidecar)
│   └── NO ↓
└── DEFAULT → Start with RBAC, evolve to ABAC/ReBAC when needed
START: Choose Token Strategy
├── Monolith architecture?
│   ├── YES → Server-side sessions (Redis) + CSRF protection
│   └── NO ↓
├── Microservices architecture?
│   ├── YES → JWT (RS256) signed by IdP, verified at gateway + service
│   └── NO ↓
├── Serverless / edge?
│   ├── YES → JWT (ES256) with short expiry (5-15min), no refresh at edge
│   └── NO ↓
└── DEFAULT → JWT (RS256) + refresh token rotation

Step-by-Step Guide

1. Set up password hashing and user registration

Hash passwords with bcrypt (cost 12+) or Argon2id before storing. Never store plaintext or MD5/SHA hashes. [src1]

// Node.js — user registration with bcrypt
const bcrypt = require('bcrypt');     // [email protected]
const SALT_ROUNDS = 12;              // ~250ms on modern hardware

async function registerUser(email, password) {
  if (password.length < 8) throw new Error('Password too short');
  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
  return db.query(
    'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id',
    [email, hashedPassword]
  );
}

Verify: await bcrypt.compare('testpass', stored_hash) → expected: true

2. Implement token issuance with JWT

Issue short-lived access tokens (15 min) and long-lived refresh tokens (7 days). Use asymmetric keys (RS256) for distributed verification. [src3]

// Node.js — JWT token issuance
const jwt = require('jsonwebtoken');  // [email protected]
const fs = require('fs');
const PRIVATE_KEY = fs.readFileSync('./keys/private.pem');

function issueTokens(user) {
  const payload = { sub: user.id, email: user.email, roles: user.roles };
  const accessToken = jwt.sign(payload, PRIVATE_KEY, {
    algorithm: 'RS256', expiresIn: '15m', issuer: 'auth.example.com',
  });
  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' }, PRIVATE_KEY,
    { algorithm: 'RS256', expiresIn: '7d', jti: crypto.randomUUID() }
  );
  return { accessToken, refreshToken };
}

Verify: Decode token at jwt.io — payload should contain sub, roles, exp fields

3. Build authentication middleware

Validate JWT on every request. Reject if expired, tampered, or using unexpected algorithm. [src1]

// Node.js Express — JWT authentication middleware
const jwt = require('jsonwebtoken');
const PUBLIC_KEY = fs.readFileSync('./keys/public.pem');

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer '))
    return res.status(401).json({ error: 'Missing bearer token' });
  try {
    req.user = jwt.verify(authHeader.slice(7), PUBLIC_KEY, {
      algorithms: ['RS256'], issuer: 'auth.example.com',
    });
    next();
  } catch (err) {
    const msg = err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token';
    res.status(401).json({ error: msg });
  }
}

Verify: Send request without token → expected: 401 Missing bearer token

4. Implement RBAC authorization middleware

Separate authorization from authentication. Check roles/permissions after identity is established. [src2]

// Node.js Express — RBAC authorization middleware
function authorize(...requiredRoles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
    const hasRole = requiredRoles.some(r => req.user.roles?.includes(r));
    if (!hasRole) return res.status(403).json({ error: 'Insufficient permissions' });
    next();
  };
}
// Usage: app.delete('/api/users/:id', authenticate, authorize('admin'), deleteUser);

Verify: Send request as viewer to admin-only route → expected: 403 Insufficient permissions

5. Add refresh token rotation

Implement refresh token rotation to limit compromise window. Invalidate old refresh tokens on use. [src3]

// Node.js — refresh token rotation
async function refreshAccessToken(refreshToken) {
  const decoded = jwt.verify(refreshToken, PUBLIC_KEY, { algorithms: ['RS256'] });
  const isRevoked = await redisClient.get(`revoked:${decoded.jti}`);
  if (isRevoked) throw new Error('Refresh token revoked — possible theft');
  await redisClient.set(`revoked:${decoded.jti}`, '1', { EX: 7 * 86400 });
  const user = await db.query('SELECT * FROM users WHERE id = $1', [decoded.sub]);
  return issueTokens(user);
}

Verify: Use same refresh token twice → second attempt should return error

6. Set up audit logging

Log all authentication events (login, logout, failed attempts, privilege changes) for compliance and incident response. [src2]

// Node.js — audit logging middleware
function auditLog(event) {
  return (req, res, next) => {
    auditStore.append({
      timestamp: new Date().toISOString(), event,
      userId: req.user?.sub || 'anonymous', ip: req.ip,
      userAgent: req.headers['user-agent'],
      resource: req.originalUrl, method: req.method,
    });
    next();
  };
}

Verify: Check audit log after login attempt → should contain event entry with IP and timestamp

Code Examples

Node.js (Express): JWT Auth Middleware with RBAC

// Input:  HTTP request with Authorization: Bearer <token>
// Output: req.user populated or 401/403 response

const jwt = require('jsonwebtoken');       // [email protected]
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;

const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });
  try {
    req.user = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
    next();
  } catch (e) { res.status(401).json({ error: 'Invalid token' }); }
};

const authorize = (...roles) => (req, res, next) => {
  if (!roles.some(r => req.user.roles?.includes(r)))
    return res.status(403).json({ error: 'Forbidden' });
  next();
};

app.delete('/users/:id', authenticate, authorize('admin'), handler);

Python (Flask): RBAC Authorization Decorator

# Input:  Flask request with JWT in Authorization header
# Output: Decorated endpoint enforces role check or returns 403

from functools import wraps
from flask import request, jsonify, g
import jwt  # PyJWT==2.x

PUBLIC_KEY = open("keys/public.pem").read()

def require_auth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get("Authorization", "").removeprefix("Bearer ")
        if not token:
            return jsonify({"error": "Missing token"}), 401
        try:
            g.user = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
        except jwt.ExpiredSignatureError:
            return jsonify({"error": "Token expired"}), 401
        except jwt.InvalidTokenError:
            return jsonify({"error": "Invalid token"}), 401
        return f(*args, **kwargs)
    return decorated

def require_role(*roles):
    def decorator(f):
        @wraps(f)
        @require_auth
        def decorated(*args, **kwargs):
            if not any(r in g.user.get("roles", []) for r in roles):
                return jsonify({"error": "Forbidden"}), 403
            return f(*args, **kwargs)
        return decorated
    return decorator

@app.route("/admin/users", methods=["DELETE"])
@require_role("admin")
def delete_user():
    pass  # Only admins reach here

Anti-Patterns

Wrong: Storing JWT secret in source code

// BAD — hardcoded secret, easily leaked via git
const token = jwt.sign(payload, 'my-super-secret-key-123', { algorithm: 'HS256' });

Correct: Load signing key from environment or vault

// GOOD — key loaded from environment, rotatable without code change
const PRIVATE_KEY = fs.readFileSync(process.env.JWT_PRIVATE_KEY_PATH);
const token = jwt.sign(payload, PRIVATE_KEY, { algorithm: 'RS256' });

Wrong: Not specifying allowed algorithms in JWT verification

// BAD — attacker can forge token with {"alg": "none"} header
const decoded = jwt.verify(token, secret);

Correct: Whitelist expected algorithms

// GOOD — rejects any token not signed with RS256
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });

Wrong: Checking permissions only on the frontend

// BAD — frontend guard is trivially bypassed
if (user.role === 'admin') {
  showDeleteButton();  // attacker calls DELETE API directly
}

Correct: Enforce authorization server-side on every request

// GOOD — server checks role regardless of frontend
app.delete('/api/users/:id',
  authenticate,                    // who are you?
  authorize('admin'),              // are you allowed?
  async (req, res) => { /* ... */ }
);

Wrong: Storing sensitive data in JWT payload

// BAD — JWT payloads are base64-encoded, NOT encrypted
const payload = { sub: id, email: e, password: pw, ssn: '123-45-6789' };

Correct: Minimal claims only

// GOOD — only non-sensitive identifiers and roles
const payload = { sub: userId, roles: ['editor'], iss: 'auth.example.com' };

Wrong: Never expiring tokens

// BAD — token valid forever, no way to revoke
const token = jwt.sign(payload, key);  // no expiresIn

Correct: Short-lived access tokens with refresh rotation

// GOOD — access token expires in 15 min, refresh token rotated on use
const accessToken = jwt.sign(payload, key, { expiresIn: '15m' });
const refreshToken = jwt.sign({ sub: userId, jti: uuid() }, key, { expiresIn: '7d' });

Common Pitfalls

Diagnostic Commands

# Decode a JWT without verification (inspect claims)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Verify JWT signature with public key (Node.js one-liner)
node -e "const jwt=require('jsonwebtoken'); const key=require('fs').readFileSync('public.pem'); console.log(jwt.verify('$TOKEN', key, {algorithms:['RS256']}))"

# Check bcrypt hash cost factor
node -e "const h='$HASH'; console.log('Cost factor:', h.split('$')[2])"

# Test login endpoint rate limiting
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.example.com/login -d '{"email":"[email protected]","password":"wrong"}'; done

# List active sessions in Redis
redis-cli keys "session:*" | head -20

# Check JWKS endpoint availability
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys | length'

Version History & Compatibility

Standard / ToolStatusKey ChangesNotes
NIST SP 800-63B-4 (2025)CurrentPhishing-resistant MFA required for AAL3; password complexity rules removedReplaces SP 800-63B
OAuth 2.1 (2025 draft)Near-finalPKCE required for all flows; implicit flow removedSupersedes OAuth 2.0 for new implementations
OAuth 2.0 (RFC 6749, 2012)ActiveStill valid; use 2.1 for new projects
JWT (RFC 7519, 2015)ActivePair with RFC 7517 (JWK) for key management
WebAuthn Level 3 (2024)CurrentConditional UI, cross-device authenticationPreferred for phishing-resistant MFA
SpiceDB 1.x (2024)ActiveZanzibar-inspired ReBACUse for large-scale relationship-based access

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Building a new application with user accountsSingle-user CLI tools or scriptsOS-level file permissions
Multiple user roles with distinct permissionsAll users have identical accessSimple API key authentication
Compliance requirements (SOC 2, HIPAA, GDPR)Internal-only prototype with no real dataSkip auth entirely for local dev
Microservices need shared identity verificationService-to-service only (no human users)mTLS or service mesh identity
Fine-grained access (ABAC/ReBAC) on resourcesFewer than 3 roles in a simple CRUD appBasic RBAC middleware is sufficient
Multi-tenant SaaS with per-org permissionsSingle-tenant, single-org deploymentSimple role column in users table

Important Caveats

Related Units