---
# === IDENTITY ===
id: software/system-design/auth-system/2026
canonical_question: "How do I design an authentication and authorization system (RBAC/ABAC)?"
aliases:
  - "auth system design RBAC ABAC"
  - "authentication authorization architecture"
  - "design role-based access control system"
  - "RBAC vs ABAC comparison"
  - "how to implement RBAC authorization"
  - "build authentication system from scratch"
  - "access control system design patterns"
  - "JWT authentication with RBAC authorization"
entity_type: software_reference
domain: software > system-design > auth_system
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-23
confidence: 0.93
version: 1.0
first_published: 2026-02-23

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "OAuth 2.1 draft finalized (2025), NIST SP 800-63B-4 (2025-07)"
  next_review: 2026-08-22
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Security-critical: Never store passwords in plaintext — use bcrypt (cost >= 12), scrypt, or Argon2id"
  - "Never put sensitive data (passwords, PII, secrets) in JWT payloads — they are base64-encoded, not encrypted"
  - "Always validate JWT signatures server-side — never trust the alg header from the token itself"
  - "Use asymmetric signing (RS256/ES256) in distributed systems — symmetric secrets (HS256) are for single-service only"
  - "Enforce MFA for privileged roles — NIST SP 800-63B requires AAL2+ for sensitive operations"
  - "Authorization checks must happen on every request at the resource level — never rely solely on frontend guards"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs OAuth/OIDC provider setup (Auth0, Okta, Keycloak configuration)"
    use_instead: "Refer to provider-specific documentation"
  - condition: "User needs API key management, not user authentication"
    use_instead: "API gateway documentation (Kong, AWS API Gateway)"
  - condition: "User needs password manager comparison"
    use_instead: "software/security/password-managers/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: auth_model
    question: "Which authorization model does your application need?"
    type: choice
    options: ["RBAC (role-based)", "ABAC (attribute-based)", "ReBAC (relationship-based)", "Not sure — help me decide"]
  - key: scale
    question: "What is the expected number of concurrent users?"
    type: choice
    options: ["<1K (small app)", "1K-100K (mid-scale)", ">100K (large-scale)"]
  - key: architecture
    question: "What is your service architecture?"
    type: choice
    options: ["Monolith", "Microservices", "Serverless"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/system-design/auth-system/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-23)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/security/password-managers/2026"
      label: "Password Managers Comparison"
  solves:
    - id: "software/debugging/ssl-tls-certificate-errors/2026"
      label: "SSL/TLS Certificate Errors"
    - id: "software/debugging/browser-cors-errors/2026"
      label: "Browser CORS Errors"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Authentication Cheat Sheet"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
    type: community_resource
    published: 2025-01-15
    reliability: authoritative
  - id: src2
    title: "Authorization Cheat Sheet"
    author: OWASP
    url: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
    type: community_resource
    published: 2025-01-15
    reliability: authoritative
  - id: src3
    title: "NIST SP 800-63B-4: Digital Identity Guidelines — Authentication and Authenticator Management"
    author: NIST
    url: https://csrc.nist.gov/pubs/sp/800/63/b/4/final
    type: rfc_spec
    published: 2025-07-31
    reliability: authoritative
  - id: src4
    title: "NIST SP 800-162: Guide to Attribute Based Access Control (ABAC)"
    author: NIST
    url: https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-162.pdf
    type: rfc_spec
    published: 2014-01-01
    reliability: authoritative
  - id: src5
    title: "What's the Right Authorization Model for My Application?"
    author: Auth0
    url: https://auth0.com/blog/whats-the-right-authorization-model-for-my-application/
    type: technical_blog
    published: 2024-06-15
    reliability: high
  - id: src6
    title: "RBAC vs. ABAC: Definitions & When to Use"
    author: Okta
    url: https://www.okta.com/identity-101/role-based-access-control-vs-attribute-based-access-control/
    type: technical_blog
    published: 2024-03-10
    reliability: high
  - id: src7
    title: "Google Zanzibar: Google's Consistent, Global Authorization System"
    author: AuthZed
    url: https://authzed.com/learn/google-zanzibar
    type: technical_blog
    published: 2024-08-20
    reliability: high
---

# Authentication & Authorization System Design (RBAC/ABAC)

## TL;DR

- **Bottom line**: Separate authentication (who are you?) from authorization (what can you do?), choose RBAC for simple role hierarchies and ABAC/ReBAC for fine-grained or relationship-based access, and enforce checks on every request server-side.
- **Key tool/command**: `jsonwebtoken` (Node.js) / `PyJWT` (Python) for token verification; dedicated policy engines (OPA, Casbin, SpiceDB) for authorization at scale.
- **Watch out for**: Mixing authentication and authorization logic in the same middleware — they must be separate concerns with separate failure modes.
- **Works with**: Any language/framework. Patterns are language-agnostic. Code examples in Node.js (Express) and Python.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never store passwords in plaintext — use bcrypt (cost >= 12), scrypt, or Argon2id [src1]
- Never put sensitive data (passwords, PII, secrets) in JWT payloads — they are base64-encoded, not encrypted [src1]
- Always validate JWT signatures server-side — never trust the `alg` header from the token itself [src2]
- Use asymmetric signing (RS256/ES256) in distributed systems — symmetric secrets (HS256) only when a single service both signs and verifies [src3]
- Enforce MFA for privileged roles — NIST SP 800-63B requires AAL2+ for sensitive operations [src3]
- Authorization checks must run on every request at the resource level — frontend guards are UX, not security [src2]

## Quick Reference

| Component | Role | Technology Options | Scaling Strategy |
|---|---|---|---|
| Identity Provider (IdP) | Authenticates users, issues tokens | Auth0, Okta, Keycloak, AWS Cognito, custom | Horizontal behind LB; federate with OIDC |
| Password Storage | Stores hashed credentials | bcrypt, Argon2id, scrypt | Tune cost factor to ~250ms per hash |
| Token Service | Issues and refreshes access/refresh tokens | JWT (RS256/ES256), opaque tokens | Short-lived access (15min), long-lived refresh (7d) |
| API Gateway | Validates tokens, rate limits, routes | Kong, AWS API Gateway, Envoy, Nginx | Horizontal; cache JWKS with TTL |
| Session Store | Stores server-side sessions or refresh tokens | Redis, Memcached, DynamoDB | Cluster mode with replication |
| RBAC Engine | Maps users to roles to permissions | Casbin, OPA, custom middleware | Cache role-permission matrix; invalidate on change |
| ABAC Engine | Evaluates attribute-based policies | OPA (Rego), Cedar (AWS), XACML | Sidecar per service; precompile policies |
| ReBAC Engine | Checks relationship graphs for access | SpiceDB, Ory Keto, OpenFGA | Distributed graph with caching (Zanzibar model) |
| MFA Service | Second-factor verification | TOTP (Google Authenticator), WebAuthn/FIDO2, SMS | Stateless TOTP; WebAuthn for phishing resistance |
| Audit Log | Records auth events for compliance | ELK stack, CloudWatch, Datadog | Append-only, immutable, separate storage |
| Secret Management | Stores signing keys, API secrets | HashiCorp Vault, AWS Secrets Manager, GCP KMS | Auto-rotate keys; zero-trust access |
| Rate Limiter | Prevents brute-force and credential stuffing | Redis + sliding window, Cloudflare WAF | Per-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]

```javascript
// Node.js — user registration with bcrypt
const bcrypt = require('bcrypt');     // bcrypt@5.x
const SALT_ROUNDS = 12;              // ~250ms on modern hardware

async function registerUser(email, password) {
  // Validate password strength (NIST: min 8 chars, check breached list)
  if (password.length < 8) throw new Error('Password too short');

  const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

  // Store email + hashedPassword in DB (never store raw password)
  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]

```javascript
// Node.js — JWT token issuance
const jwt = require('jsonwebtoken');  // jsonwebtoken@9.x
const fs = require('fs');

const PRIVATE_KEY = fs.readFileSync('./keys/private.pem');
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';

function issueTokens(user) {
  const payload = {
    sub: user.id,
    email: user.email,
    roles: user.roles,            // e.g., ["editor", "viewer"]
  };

  const accessToken = jwt.sign(payload, PRIVATE_KEY, {
    algorithm: 'RS256',
    expiresIn: ACCESS_TOKEN_EXPIRY,
    issuer: 'auth.example.com',
  });

  const refreshToken = jwt.sign({ sub: user.id, type: 'refresh' }, PRIVATE_KEY, {
    algorithm: 'RS256',
    expiresIn: REFRESH_TOKEN_EXPIRY,
    jti: crypto.randomUUID(),       // unique ID for revocation
  });

  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]

```javascript
// Node.js Express — JWT authentication middleware
const jwt = require('jsonwebtoken');
const fs = require('fs');
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' });
  }

  const token = authHeader.slice(7);
  try {
    // CRITICAL: specify algorithms to prevent "none" algorithm attack
    const decoded = jwt.verify(token, PUBLIC_KEY, {
      algorithms: ['RS256'],       // whitelist ONLY expected algorithm
      issuer: 'auth.example.com',
    });
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}
```

**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]

```javascript
// 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 userRoles = req.user.roles || [];
    const hasRole = requiredRoles.some(role => userRoles.includes(role));
    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 the window of compromise. Invalidate old refresh tokens on use. [src3]

```javascript
// Node.js — refresh token rotation
async function refreshAccessToken(refreshToken) {
  const decoded = jwt.verify(refreshToken, PUBLIC_KEY, {
    algorithms: ['RS256'],
  });

  // Check if refresh token has been revoked (stored in Redis/DB)
  const isRevoked = await redisClient.get(`revoked:${decoded.jti}`);
  if (isRevoked) throw new Error('Refresh token revoked — possible theft');

  // Revoke the old refresh token (one-time use)
  await redisClient.set(`revoked:${decoded.jti}`, '1', { EX: 7 * 86400 });

  // Issue new token pair
  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]

```javascript
// Node.js — audit logging middleware
function auditLog(event) {
  return (req, res, next) => {
    const logEntry = {
      timestamp: new Date().toISOString(),
      event,
      userId: req.user?.sub || 'anonymous',
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      resource: req.originalUrl,
      method: req.method,
    };
    // Append to immutable audit log (never delete)
    auditStore.append(logEntry);
    next();
  };
}
```

**Verify**: Check audit log after login attempt --> should contain event entry with IP and timestamp

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### Node.js (Express): JWT Auth Middleware with RBAC

```javascript
// Input:  HTTP request with Authorization: Bearer <token>
// Output: req.user populated or 401/403 response

const jwt = require('jsonwebtoken');       // jsonwebtoken@9.x
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY;

// Authentication: verify identity
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' });
  }
};

// Authorization: check roles
const authorize = (...roles) => (req, res, next) => {
  if (!roles.some(r => req.user.roles?.includes(r)))
    return res.status(403).json({ error: 'Forbidden' });
  next();
};

// Route: only admins can delete users
app.delete('/users/:id', authenticate, authorize('admin'), handler);
```

### Python: RBAC Authorization Decorator

> Full script: [python-rbac-authorization-decorator.py](scripts/python-rbac-authorization-decorator.py) (39 lines)

```python
# 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
# ... (see full script)
```

## Anti-Patterns

### Wrong: Storing JWT secret in source code

```javascript
// 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

```javascript
// 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

```javascript
// BAD — attacker can forge token with {"alg": "none"} header
const decoded = jwt.verify(token, secret);
```

### Correct: Whitelist expected algorithms

```javascript
// GOOD — rejects any token not signed with RS256
const decoded = jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
```

### Wrong: Checking permissions only on the frontend

```javascript
// BAD — frontend guard is trivially bypassed
if (user.role === 'admin') {
  showDeleteButton();  // attacker calls DELETE API directly
}
```

### Correct: Enforce authorization server-side on every request

```javascript
// 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

```javascript
// BAD — JWT payloads are base64-encoded, NOT encrypted
const payload = { sub: userId, email: email, password: password, ssn: '123-45-6789' };
```

### Correct: Minimal claims only

```javascript
// GOOD — only non-sensitive identifiers and roles
const payload = { sub: userId, roles: ['editor'], iss: 'auth.example.com' };
```

### Wrong: Never expiring tokens

```javascript
// BAD — token valid forever, no way to revoke
const token = jwt.sign(payload, key);  // no expiresIn
```

### Correct: Short-lived access tokens with refresh rotation

```javascript
// 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

- **Role explosion in RBAC**: Creating a new role for every permission combination leads to hundreds of unmanageable roles. Fix: use permission-based RBAC (assign permissions to roles, not directly to users) or switch to ABAC. [src5]
- **No token revocation strategy**: JWTs cannot be invalidated before expiry unless you maintain a blocklist. Fix: keep access tokens short-lived (15 min) and maintain a Redis-based revocation list for refresh tokens. [src3]
- **Symmetric JWT signing in microservices**: Using HS256 means every service needs the shared secret, increasing the blast radius if one service is compromised. Fix: use RS256/ES256 — services only need the public key to verify. [src1]
- **Missing rate limiting on login endpoints**: Without rate limits, attackers can brute-force credentials. Fix: implement per-IP and per-account rate limiting (e.g., 5 failed attempts per minute, then exponential backoff). [src1]
- **Confusing authentication with authorization**: Verifying identity does not grant access. A valid token proves who the user is, not what they can do. Fix: always separate authn middleware from authz middleware. [src2]
- **Not rotating signing keys**: Using the same JWT signing key for years means a single key compromise invalidates all tokens permanently. Fix: implement key rotation with JWKS endpoint; support multiple active keys during transition. [src3]
- **Trusting the client for role assignments**: Allowing users to set their own role in registration or profile update. Fix: roles must be assigned server-side by an admin or automated provisioning system. [src2]
- **Forgetting logout invalidation**: Logout on the client (deleting the token) is not true logout — the token is still valid. Fix: server-side revocation of refresh token on logout; short access token expiry. [src1]

## Diagnostic Commands

```bash
# 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":"test@test.com","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 / Tool | Status | Key Changes | Notes |
|---|---|---|---|
| NIST SP 800-63B-4 (2025) | Current | Phishing-resistant MFA required for AAL3; password complexity rules removed | Replaces SP 800-63B |
| OAuth 2.1 (2025 draft) | Near-final | PKCE required for all flows; implicit flow removed | Supersedes OAuth 2.0 for new implementations |
| OAuth 2.0 (RFC 6749, 2012) | Active (widely deployed) | — | Still valid; use 2.1 for new projects |
| JWT (RFC 7519, 2015) | Active | — | Pair with RFC 7517 (JWK) for key management |
| WebAuthn Level 3 (2024) | Current | Conditional UI, cross-device authentication | Preferred for phishing-resistant MFA |
| SpiceDB 1.x (2024) | Active | Zanzibar-inspired ReBAC | Use for large-scale relationship-based access |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building a new application with user accounts | Single-user CLI tools or scripts | OS-level file permissions |
| Multiple user roles with distinct permissions | All users have identical access | Simple API key authentication |
| Compliance requirements (SOC 2, HIPAA, GDPR) | Internal-only prototype with no real data | Skip auth entirely for local dev |
| Microservices need shared identity verification | Service-to-service only (no human users) | mTLS or service mesh identity |
| Fine-grained access (ABAC/ReBAC) on resources | Fewer than 3 roles in a simple CRUD app | Basic RBAC middleware is sufficient |
| Multi-tenant SaaS with per-org permissions | Single-tenant, single-org deployment | Simple role column in users table |

## Important Caveats

- RBAC, ABAC, and ReBAC are not mutually exclusive — most production systems use a hybrid (e.g., RBAC for coarse access + ABAC for fine-grained policies)
- JWT is an authentication token format, not an authorization framework — you still need a separate authorization layer
- OAuth 2.0 is an authorization framework, not an authentication protocol — use OIDC (OpenID Connect) on top of OAuth for authentication
- Password rotation policies are no longer recommended by NIST SP 800-63B-4 — they lead to weaker passwords; check against breached password databases instead
- Session-based auth (server-side) is still valid and often simpler than JWT for monoliths — JWT is primarily beneficial in distributed/stateless architectures
- All authorization models require careful cache invalidation — stale role/permission caches are a common source of privilege escalation bugs

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Password Managers Comparison](/software/security/password-managers/2026)
- [SSL/TLS Certificate Errors](/software/debugging/ssl-tls-certificate-errors/2026)
- [Browser CORS Errors](/software/debugging/browser-cors-errors/2026)
