|$ curl https://forge-ai.dev/api/markdown?path=docs/security/auth
$cat docs/security-—-authentication-&-authorization.md
updated Recently·16 min read·published

Security — Authentication & Authorization

SecurityAdvanced
Introduction

Authentication verifies who the user is; authorization determines what they can do. Together they form the access control layer of every application. Getting either wrong can lead to data breaches, privilege escalation, and unauthorized access.

Modern applications use a mix of strategies: JWTs for stateless APIs, sessions for server-rendered apps, OAuth2 for delegated authorization, and MFA for high-security scenarios. This guide covers the most important patterns and their secure implementation.

Authentication vs Authorization
AspectAuthenticationAuthorization
Question answered"Who are you?""What can you do?"
Common methodsPassword, JWT, OAuth, biometricRBAC, ABAC, ACL, permissions
When it happensFirst (login)After authentication
GranularityBinary (authenticated or not)Fine-grained (per-resource)
Failure result401 Unauthorized403 Forbidden
JWT (JSON Web Tokens)

JWTs are a stateless authentication mechanism consisting of three parts: header, payload, and signature. The token is self-contained — it carries the user identity and claims, signed by the server to prevent tampering.

jwt-auth.ts
TypeScript
1// JWT structure: header.payload.signature
2// Header: algorithm and token type
3{
4 "alg": "RS256",
5 "typ": "JWT",
6 "kid": "key-id-1"
7}
8
9// Payload: claims about the user
10{
11 "sub": "user_12345",
12 "name": "Alice Smith",
13 "email": "alice@example.com",
14 "iat": 1720000000,
15 "exp": 1720003600,
16 "iss": "https://auth.example.com",
17 "aud": "https://api.example.com",
18 "roles": ["admin", "editor"]
19}
20
21// Signature: prevents tampering
22// HMACSHA256(
23// base64UrlEncode(header) + "." +
24// base64UrlEncode(payload),
25// secret
26// )
27
28// JWT creation (Node.js with jsonwebtoken)
29import jwt from 'jsonwebtoken';
30
31const payload = {
32 sub: 'user_12345',
33 email: 'alice@example.com',
34 roles: ['admin', 'editor'],
35};
36
37const token = jwt.sign(payload, process.env.JWT_SECRET, {
38 algorithm: 'HS256',
39 expiresIn: '1h',
40 issuer: 'https://auth.example.com',
41 audience: 'https://api.example.com',
42});
43
44// JWT verification
45try {
46 const decoded = jwt.verify(token, process.env.JWT_SECRET, {
47 algorithms: ['HS256'], // Explicit algorithm whitelist
48 issuer: 'https://auth.example.com',
49 audience: 'https://api.example.com',
50 });
51 console.log('Authenticated user:', decoded.sub);
52} catch (err) {
53 // Token expired, invalid signature, or wrong issuer
54 console.error('Auth failed:', err.message);
55}
56
57// Use RS256 (asymmetric) for production:
58// Private key signs, public key verifies
59// import { privateKey, publicKey } from './keys';
60// const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
61// const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });

danger

Always whitelist the allowed algorithms when verifying JWTs. If you use jwt.verify(token, secret) without specifying algorithms, an attacker can change the algorithm to none and bypass verification. Use RS256 over HS256 in production to avoid shared secret compromise.
Session-Based Authentication

Session-based auth stores session data server-side (typically in Redis or a database) and sends the client a session ID cookie. The server looks up the session on each request. This approach offers easy revocation and control.

session-auth.ts
TypeScript
1// Session-based auth with Express + Redis
2import express from 'express';
3import session from 'express-session';
4import RedisStore from 'connect-redis';
5import { createClient } from 'redis';
6
7const app = express();
8
9// Redis client for session storage
10const redisClient = createClient({ url: process.env.REDIS_URL });
11await redisClient.connect();
12
13// Session configuration
14app.use(session({
15 store: new RedisStore({ client: redisClient }),
16 secret: process.env.SESSION_SECRET, // Sign the session ID cookie
17 resave: false,
18 saveUninitialized: false,
19 name: 'sid', // Custom cookie name (not default 'connect.sid')
20 cookie: {
21 httpOnly: true, // Inaccessible to JavaScript
22 secure: process.env.NODE_ENV === 'production',
23 sameSite: 'lax',
24 maxAge: 24 * 60 * 60 * 1000, // 24 hours
25 path: '/',
26 domain: process.env.COOKIE_DOMAIN,
27 },
28}));
29
30// Login — create session
31app.post('/login', async (req, res) => {
32 const { email, password } = req.body;
33 const user = await authenticateUser(email, password);
34 if (!user) {
35 return res.status(401).json({ error: 'Invalid credentials' });
36 }
37
38 // Store user data in session (minimal)
39 req.session.userId = user.id;
40 req.session.roles = user.roles;
41 req.session.createdAt = Date.now();
42
43 res.json({ message: 'Logged in' });
44});
45
46// Auth middleware
47function requireAuth(req, res, next) {
48 if (!req.session?.userId) {
49 return res.status(401).json({ error: 'Authentication required' });
50 }
51 next();
52}
53
54// Protected route
55app.get('/api/profile', requireAuth, async (req, res) => {
56 const user = await getUserById(req.session.userId);
57 res.json(user);
58});
59
60// Logout — destroy session
61app.post('/logout', (req, res) => {
62 req.session.destroy((err) => {
63 if (err) return res.status(500).json({ error: 'Logout failed' });
64 res.clearCookie('sid', { path: '/' });
65 res.json({ message: 'Logged out' });
66 });
67});
68
69// Session regeneration (after login/privilege escalation)
70req.session.regenerate((err) => {
71 // Prevents session fixation attacks
72});

info

Regenerate the session ID after login and privilege changes to prevent session fixation attacks. Use a custom cookie name instead of the default to make it harder for attackers to identify the framework. Store only essential data in the session — keep database lookups for full user data.
OAuth2 Flow

OAuth2 is the industry standard for delegated authorization. It allows applications to access resources on behalf of a user without exposing their credentials. The Authorization Code flow with PKCE is the recommended approach for single-page and mobile applications.

oauth2-pkce.ts
TypeScript
1// OAuth2 Authorization Code + PKCE Flow
2// Step 1: Generate code verifier and challenge (SPA)
3import { generateCodeVerifier, generateCodeChallenge } from './pkce';
4
5const verifier = generateCodeVerifier(); // Random string
6const challenge = generateCodeChallenge(verifier); // SHA256 hash
7
8// Store verifier in local state (not localStorage!)
9sessionStorage.setItem('pkce_verifier', verifier);
10
11// Step 2: Redirect to authorization server
12const authUrl = new URL('https://auth.example.com/authorize');
13authUrl.searchParams.set('response_type', 'code');
14authUrl.searchParams.set('client_id', process.env.CLIENT_ID);
15authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
16authUrl.searchParams.set('code_challenge', challenge);
17authUrl.searchParams.set('code_challenge_method', 'S256');
18authUrl.searchParams.set('scope', 'openid profile email');
19authUrl.searchParams.set('state', crypto.randomUUID()); // CSRF protection
20
21window.location.href = authUrl.toString();
22
23// Step 3: Exchange authorization code for tokens (server-side)
24// Endpoint: POST /oauth/token
25async function exchangeCode(code: string, verifier: string) {
26 const response = await fetch('https://auth.example.com/oauth/token', {
27 method: 'POST',
28 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
29 body: new URLSearchParams({
30 grant_type: 'authorization_code',
31 client_id: process.env.CLIENT_ID,
32 client_secret: process.env.CLIENT_SECRET,
33 code,
34 redirect_uri: 'https://app.example.com/callback',
35 code_verifier: verifier,
36 }),
37 });
38
39 const tokens = await response.json();
40 // tokens.access_token, tokens.refresh_token, tokens.id_token
41 return tokens;
42}
43
44// Step 4: Use access token for API requests
45fetch('https://api.example.com/user', {
46 headers: {
47 'Authorization': `Bearer ${accessToken}`,
48 },
49});
50
51// Step 5: Refresh tokens
52async function refreshAccessToken(refreshToken: string) {
53 const response = await fetch('https://auth.example.com/oauth/token', {
54 method: 'POST',
55 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
56 body: new URLSearchParams({
57 grant_type: 'refresh_token',
58 client_id: process.env.CLIENT_ID,
59 client_secret: process.env.CLIENT_SECRET,
60 refresh_token: refreshToken,
61 }),
62 });
63
64 return response.json();
65}

best practice

Always use PKCE (Proof Key for Code Exchange) for public clients like SPAs and mobile apps. The implicit grant (returning tokens directly in the redirect) is deprecated and should not be used. Store the code_verifier in memory or sessionStorage, not localStorage.
OpenID Connect (OIDC)

OIDC is an identity layer on top of OAuth2. It adds an id_token (a JWT containing user identity information) and the userinfo endpoint. While OAuth2 is for authorization (delegating access), OIDC is for authentication (verifying identity).

oidc-verify.ts
TypeScript
1// OpenID Connect — ID token verification (Node.js)
2import jwt from 'jsonwebtoken';
3import jwksClient from 'jwks-rsa';
4
5// Fetch public keys from the OIDC provider
6const client = jwksClient({
7 jwksUri: 'https://auth.example.com/.well-known/jwks.json',
8});
9
10function getSigningKey(kid: string): Promise<string> {
11 return new Promise((resolve, reject) => {
12 client.getSigningKey(kid, (err, key) => {
13 if (err) return reject(err);
14 resolve(key.getPublicKey());
15 });
16 });
17}
18
19// Verify ID token
20async function verifyIdToken(idToken: string): Promise<JWTPayload> {
21 const decoded = jwt.decode(idToken, { complete: true });
22 const kid = decoded.header.kid;
23 const publicKey = await getSigningKey(kid);
24
25 return jwt.verify(idToken, publicKey, {
26 algorithms: ['RS256'],
27 issuer: 'https://auth.example.com',
28 audience: process.env.CLIENT_ID, // Must be your client ID
29 });
30}
31
32// OIDC userinfo endpoint
33async function getUserInfo(accessToken: string) {
34 const response = await fetch('https://auth.example.com/userinfo', {
35 headers: { 'Authorization': `Bearer ${accessToken}` },
36 });
37 return response.json();
38}
39
40// OIDC well-known configuration endpoint
41// GET https://auth.example.com/.well-known/openid-configuration
42// Returns: authorization_endpoint, token_endpoint, userinfo_endpoint,
43// jwks_uri, scopes_supported, claims_supported, etc.
Passwordless Authentication

Passwordless auth eliminates passwords in favor of magic links, one-time codes, or biometric authentication. It reduces phishing risk and improves user experience.

passwordless-auth.ts
TypeScript
1// Magic link authentication (Node.js)
2import crypto from 'crypto';
3import { sendEmail } from './email';
4
5// Generate magic link token
6function createMagicToken(userId: string): { token: string; expires: Date } {
7 const token = crypto.randomBytes(32).toString('hex');
8 const expires = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
9
10 // Store token hash (not raw token) in database
11 const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
12 // db.save({ userId, tokenHash, expires });
13
14 return { token, expires };
15}
16
17// Send magic link email
18async function sendMagicLink(email: string) {
19 const user = await getUserByEmail(email);
20 if (!user) return; // Don't reveal whether email exists
21
22 const { token } = createMagicToken(user.id);
23 const link = `https://app.example.com/auth/verify?token=${token}`;
24
25 await sendEmail({
26 to: email,
27 subject: 'Sign in to Example App',
28 html: `<a href="${link}">Sign in</a> (link expires in 15 minutes)`,
29 });
30}
31
32// Verify magic link
33app.get('/auth/verify', async (req, res) => {
34 const { token } = req.query;
35 const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
36 const session = await db.findMagicToken(tokenHash);
37
38 if (!session || session.expires < new Date()) {
39 return res.status(401).send('Invalid or expired link');
40 }
41
42 // Create user session
43 req.session.userId = session.userId;
44 await db.deleteMagicToken(tokenHash); // Single-use
45 res.redirect('/dashboard');
46});
47
48// WebAuthn (Passkeys) — browser-based biometric auth
49// Registration
50const credential = await navigator.credentials.create({
51 publicKey: {
52 challenge: new Uint8Array(32),
53 rp: { name: 'Example App', id: 'example.com' },
54 user: { id: new Uint8Array(16), name: 'alice@example.com', displayName: 'Alice' },
55 pubKeyCredParams: [{ type: 'public-key', alg: -7 }], // ES256
56 },
57});
58
59// Authentication
60const assertion = await navigator.credentials.get({
61 publicKey: {
62 challenge: new Uint8Array(32),
63 allowCredentials: [{ type: 'public-key', id: credentialId }],
64 },
65});

warning

Magic links should expire within 15 minutes and be single-use. Never reveal whether an email is registered in your system (return the same response regardless). Hash tokens before storing them in the database — if your DB is compromised, raw tokens should not be exposed.
Multi-Factor Authentication (MFA)

MFA requires two or more factors: something you know (password), something you have (phone, hardware key), and something you are (biometric). TOTP (Time-Based One-Time Passwords) is the most common software-based MFA.

totp-mfa.ts
TypeScript
1// TOTP MFA setup and verification (Node.js)
2import { authenticator } from 'otplib';
3import QRCode from 'qrcode';
4
5// Generate a shared secret for the user
6const secret = authenticator.generateSecret();
7// Store the secret securely (encrypted) in your database
8// db.save({ userId, totpSecret: encrypt(secret) });
9
10// Generate provisioning URI for authenticator app
11const service = 'Example App';
12const userEmail = 'alice@example.com';
13const otpauth = authenticator.keyuri(userEmail, service, secret);
14
15// Display as QR code for the user to scan
16const qrCode = await QRCode.toDataURL(otpauth);
17
18// Verify TOTP code
19function verifyTotp(token: string, userSecret: string): boolean {
20 try {
21 return authenticator.verify({
22 token,
23 secret: userSecret,
24 });
25 } catch {
26 return false;
27 }
28}
29
30// MFA enrollment endpoint
31app.post('/mfa/enroll', async (req, res) => {
32 const { userId } = req.session;
33 const secret = authenticator.generateSecret();
34 const otpauth = authenticator.keyuri(req.session.email, service, secret);
35
36 // Store temporarily until verified
37 req.session.pendingMfaSecret = secret;
38
39 res.json({
40 secret,
41 qrCode: await QRCode.toDataURL(otpauth),
42 });
43});
44
45// MFA verification (complete enrollment)
46app.post('/mfa/verify', async (req, res) => {
47 const isValid = verifyTotp(req.body.token, req.session.pendingMfaSecret);
48 if (!isValid) {
49 return res.status(400).json({ error: 'Invalid code' });
50 }
51
52 await enableMfa(req.session.userId, req.session.pendingMfaSecret);
53 delete req.session.pendingMfaSecret;
54 res.json({ message: 'MFA enabled' });
55});
56
57// MFA login middleware
58app.post('/login', async (req, res) => {
59 const user = await authenticateUser(req.body.email, req.body.password);
60 if (!user) return res.status(401).json({ error: 'Invalid credentials' });
61
62 if (user.mfaEnabled) {
63 // Generate MFA session token (short-lived, different from full session)
64 const mfaToken = crypto.randomBytes(32).toString('hex');
65 req.session.mfaToken = mfaToken;
66 req.session.pendingUserId = user.id;
67 return res.json({ mfaRequired: true, mfaToken });
68 }
69
70 req.session.userId = user.id;
71 res.json({ message: 'Logged in' });
72});
73
74// Verify MFA during login
75app.post('/login/mfa', async (req, res) => {
76 if (!req.session.mfaToken || !req.session.pendingUserId) {
77 return res.status(401).json({ error: 'No pending login' });
78 }
79
80 const user = await getUserById(req.session.pendingUserId);
81 const isValid = verifyTotp(req.body.token, user.totpSecret);
82
83 if (!isValid) return res.status(401).json({ error: 'Invalid MFA code' });
84
85 req.session.userId = user.id;
86 delete req.session.mfaToken;
87 delete req.session.pendingUserId;
88 res.json({ message: 'Authenticated' });
89});
RBAC (Role-Based Access Control)

RBAC assigns permissions to roles, and users are assigned roles. This simplifies permission management — instead of assigning permissions per-user, you manage roles. Users inherit the permissions of their roles.

rbac.ts
TypeScript
1// RBAC implementation
2type Role = 'admin' | 'editor' | 'viewer' | 'moderator';
3type Permission =
4 | 'post:create' | 'post:read' | 'post:update' | 'post:delete'
5 | 'user:create' | 'user:read' | 'user:update' | 'user:delete'
6 | 'settings:read' | 'settings:update';
7
8// Role-permission mapping
9const rolePermissions: Record<Role, Permission[]> = {
10 admin: [
11 'post:create', 'post:read', 'post:update', 'post:delete',
12 'user:create', 'user:read', 'user:update', 'user:delete',
13 'settings:read', 'settings:update',
14 ],
15 editor: [
16 'post:create', 'post:read', 'post:update', 'post:delete',
17 ],
18 moderator: [
19 'post:read', 'post:update', 'post:delete',
20 ],
21 viewer: [
22 'post:read',
23 ],
24};
25
26// Check permission middleware
27function requirePermission(permission: Permission) {
28 return (req, res, next) => {
29 const userRole: Role = req.session.role;
30
31 if (!userRole) {
32 return res.status(401).json({ error: 'Not authenticated' });
33 }
34
35 const permissions = rolePermissions[userRole];
36 if (!permissions?.includes(permission)) {
37 return res.status(403).json({ error: 'Insufficient permissions' });
38 }
39
40 next();
41 };
42}
43
44// Usage in routes
45app.post('/posts', requirePermission('post:create'), (req, res) => {
46 // Create post
47});
48
49app.delete('/users/:id', requirePermission('user:delete'), (req, res) => {
50 // Delete user
51});
52
53// Helper to check permissions in code
54function hasPermission(userRole: Role, permission: Permission): boolean {
55 return rolePermissions[userRole]?.includes(permission) ?? false;
56}
ABAC (Attribute-Based Access Control)

ABAC evaluates access based on attributes of the user, resource, action, and environment. It is more flexible than RBAC but more complex to implement. Policies are expressed as boolean rules.

abac.ts
TypeScript
1// ABAC policy engine
2interface AccessRequest {
3 user: {
4 id: string;
5 role: Role;
6 department: string;
7 clearanceLevel: number;
8 };
9 resource: {
10 type: string;
11 ownerId: string;
12 classification: string;
13 department: string;
14 };
15 action: string;
16 context: {
17 ip: string;
18 time: Date;
19 location: string;
20 };
21}
22
23type PolicyRule = (request: AccessRequest) => boolean;
24
25// Policy definitions
26const policies: PolicyRule[] = [
27 // Admins can access anything
28 (req) => req.user.role === 'admin',
29
30 // Users can access their own resources
31 (req) => req.resource.ownerId === req.user.id,
32
33 // Department members can read department resources
34 (req) =>
35 req.action === 'read' &&
36 req.resource.department === req.user.department,
37
38 // High-clearance users can access classified documents
39 (req) =>
40 req.resource.classification === 'confidential' &&
41 req.user.clearanceLevel >= 3,
42
43 // Time-based restriction
44 (req) =>
45 req.action === 'delete' &&
46 req.context.time.getHours() >= 9 &&
47 req.context.time.getHours() < 17,
48];
49
50function checkAccess(request: AccessRequest): boolean {
51 // Allow if any policy matches (whitelist approach)
52 return policies.some((policy) => policy(request));
53}
54
55// Middleware
56function abacMiddleware(resourceType: string, action: string) {
57 return (req, res, next) => {
58 const accessRequest: AccessRequest = {
59 user: {
60 id: req.session.userId,
61 role: req.session.role,
62 department: req.session.department,
63 clearanceLevel: req.session.clearanceLevel,
64 },
65 resource: {
66 type: resourceType,
67 ownerId: req.params.userId || req.body.userId,
68 classification: req.body.classification || 'public',
69 department: req.resourceDepartment,
70 },
71 action,
72 context: {
73 ip: req.ip,
74 time: new Date(),
75 location: req.headers['cloudfront-viewer-country'],
76 },
77 };
78
79 if (!checkAccess(accessRequest)) {
80 return res.status(403).json({ error: 'Access denied' });
81 }
82 next();
83 };
84}
85
86app.get('/api/documents/:id', abacMiddleware('document', 'read'), handler);
87app.delete('/api/documents/:id', abacMiddleware('document', 'delete'), handler);
Token Storage Best Practices
Storage MethodXSS RiskCSRF RiskPersistenceUse Case
httpOnly CookieNoneVulnerableSession-basedRecommended for most apps
localStorageVulnerableNonePersistentAvoid if possible
sessionStorageVulnerableNoneTab-scopedShort-lived SPA tokens
Memory (variable)NoneNoneLost on refreshMost secure, least persistent

best practice

Prefer httpOnly cookies for session tokens. They are inaccessible to JavaScript, eliminating XSS-based token theft. Use SameSite=Strict or Lax plus CSRF tokens to mitigate CSRF. If you must use localStorage (e.g., for access tokens in an SPA), set short expiration times and implement token refresh.
Common Auth Vulnerabilities
  • Brute Force: Implement rate limiting and account lockout after N failed attempts.
  • Credential Stuffing: Use leaked password detection (Have I Been Pwned API) and require MFA.
  • Session Fixation: Regenerate session ID after login and privilege changes.
  • JWT None Algorithm: Always whitelist algorithms on the server when verifying tokens.
  • Token Leakage: Never log tokens, include them in URLs, or store them in insecure locations.
  • Privilege Escalation: Verify authorization on every request, not just at login.
  • Insecure Direct Object References (IDOR): Check ownership for every resource access.
  • Weak Password Policy: Enforce minimum complexity, use bcrypt/argon2 for hashing.
Best Practices
  • Use bcrypt or argon2 for password hashing — never use MD5, SHA1, or unsalted SHA256.
  • Implement account lockout (temporary) rather than permanent disable to prevent DoS.
  • Use OAuth2 with PKCE for third-party login — never implement your own OAuth provider.
  • Set access token lifetimes to 15-60 minutes and use refresh tokens for longer sessions.
  • Validate all tokens server-side on every request — never trust client-side claims alone.
  • Log all authentication events (login, logout, failed attempts, MFA, privilege changes).
  • Use crypto.timingSafeEqual for comparing tokens and secrets.
  • Separate authentication logic from business logic — use middleware for auth checks.

info

Consider using a managed auth service (Auth0, Clerk, Supabase Auth, Firebase Auth) for production applications. They handle edge cases like session rotation, breach detection, and MFA enrollment flows that are easy to get wrong in a custom implementation.
$Blueprint — Engineering Documentation·Section ID: SEC-AUTH·Revision: 1.0