Security — Authentication & Authorization
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.
| Aspect | Authentication | Authorization |
|---|---|---|
| Question answered | "Who are you?" | "What can you do?" |
| Common methods | Password, JWT, OAuth, biometric | RBAC, ABAC, ACL, permissions |
| When it happens | First (login) | After authentication |
| Granularity | Binary (authenticated or not) | Fine-grained (per-resource) |
| Failure result | 401 Unauthorized | 403 Forbidden |
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.
| 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) |
| 29 | import jwt from 'jsonwebtoken'; |
| 30 | |
| 31 | const payload = { |
| 32 | sub: 'user_12345', |
| 33 | email: 'alice@example.com', |
| 34 | roles: ['admin', 'editor'], |
| 35 | }; |
| 36 | |
| 37 | const 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 |
| 45 | try { |
| 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
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.
| 1 | // Session-based auth with Express + Redis |
| 2 | import express from 'express'; |
| 3 | import session from 'express-session'; |
| 4 | import RedisStore from 'connect-redis'; |
| 5 | import { createClient } from 'redis'; |
| 6 | |
| 7 | const app = express(); |
| 8 | |
| 9 | // Redis client for session storage |
| 10 | const redisClient = createClient({ url: process.env.REDIS_URL }); |
| 11 | await redisClient.connect(); |
| 12 | |
| 13 | // Session configuration |
| 14 | app.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 |
| 31 | app.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 |
| 47 | function 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 |
| 55 | app.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 |
| 61 | app.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) |
| 70 | req.session.regenerate((err) => { |
| 71 | // Prevents session fixation attacks |
| 72 | }); |
info
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.
| 1 | // OAuth2 Authorization Code + PKCE Flow |
| 2 | // Step 1: Generate code verifier and challenge (SPA) |
| 3 | import { generateCodeVerifier, generateCodeChallenge } from './pkce'; |
| 4 | |
| 5 | const verifier = generateCodeVerifier(); // Random string |
| 6 | const challenge = generateCodeChallenge(verifier); // SHA256 hash |
| 7 | |
| 8 | // Store verifier in local state (not localStorage!) |
| 9 | sessionStorage.setItem('pkce_verifier', verifier); |
| 10 | |
| 11 | // Step 2: Redirect to authorization server |
| 12 | const authUrl = new URL('https://auth.example.com/authorize'); |
| 13 | authUrl.searchParams.set('response_type', 'code'); |
| 14 | authUrl.searchParams.set('client_id', process.env.CLIENT_ID); |
| 15 | authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback'); |
| 16 | authUrl.searchParams.set('code_challenge', challenge); |
| 17 | authUrl.searchParams.set('code_challenge_method', 'S256'); |
| 18 | authUrl.searchParams.set('scope', 'openid profile email'); |
| 19 | authUrl.searchParams.set('state', crypto.randomUUID()); // CSRF protection |
| 20 | |
| 21 | window.location.href = authUrl.toString(); |
| 22 | |
| 23 | // Step 3: Exchange authorization code for tokens (server-side) |
| 24 | // Endpoint: POST /oauth/token |
| 25 | async 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 |
| 45 | fetch('https://api.example.com/user', { |
| 46 | headers: { |
| 47 | 'Authorization': `Bearer ${accessToken}`, |
| 48 | }, |
| 49 | }); |
| 50 | |
| 51 | // Step 5: Refresh tokens |
| 52 | async 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
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).
| 1 | // OpenID Connect — ID token verification (Node.js) |
| 2 | import jwt from 'jsonwebtoken'; |
| 3 | import jwksClient from 'jwks-rsa'; |
| 4 | |
| 5 | // Fetch public keys from the OIDC provider |
| 6 | const client = jwksClient({ |
| 7 | jwksUri: 'https://auth.example.com/.well-known/jwks.json', |
| 8 | }); |
| 9 | |
| 10 | function 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 |
| 20 | async 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 |
| 33 | async 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 auth eliminates passwords in favor of magic links, one-time codes, or biometric authentication. It reduces phishing risk and improves user experience.
| 1 | // Magic link authentication (Node.js) |
| 2 | import crypto from 'crypto'; |
| 3 | import { sendEmail } from './email'; |
| 4 | |
| 5 | // Generate magic link token |
| 6 | function 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 |
| 18 | async 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 |
| 33 | app.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 |
| 50 | const 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 |
| 60 | const assertion = await navigator.credentials.get({ |
| 61 | publicKey: { |
| 62 | challenge: new Uint8Array(32), |
| 63 | allowCredentials: [{ type: 'public-key', id: credentialId }], |
| 64 | }, |
| 65 | }); |
warning
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.
| 1 | // TOTP MFA setup and verification (Node.js) |
| 2 | import { authenticator } from 'otplib'; |
| 3 | import QRCode from 'qrcode'; |
| 4 | |
| 5 | // Generate a shared secret for the user |
| 6 | const 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 |
| 11 | const service = 'Example App'; |
| 12 | const userEmail = 'alice@example.com'; |
| 13 | const otpauth = authenticator.keyuri(userEmail, service, secret); |
| 14 | |
| 15 | // Display as QR code for the user to scan |
| 16 | const qrCode = await QRCode.toDataURL(otpauth); |
| 17 | |
| 18 | // Verify TOTP code |
| 19 | function 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 |
| 31 | app.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) |
| 46 | app.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 |
| 58 | app.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 |
| 75 | app.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 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.
| 1 | // RBAC implementation |
| 2 | type Role = 'admin' | 'editor' | 'viewer' | 'moderator'; |
| 3 | type 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 |
| 9 | const 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 |
| 27 | function 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 |
| 45 | app.post('/posts', requirePermission('post:create'), (req, res) => { |
| 46 | // Create post |
| 47 | }); |
| 48 | |
| 49 | app.delete('/users/:id', requirePermission('user:delete'), (req, res) => { |
| 50 | // Delete user |
| 51 | }); |
| 52 | |
| 53 | // Helper to check permissions in code |
| 54 | function hasPermission(userRole: Role, permission: Permission): boolean { |
| 55 | return rolePermissions[userRole]?.includes(permission) ?? false; |
| 56 | } |
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.
| 1 | // ABAC policy engine |
| 2 | interface 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 | |
| 23 | type PolicyRule = (request: AccessRequest) => boolean; |
| 24 | |
| 25 | // Policy definitions |
| 26 | const 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 | |
| 50 | function checkAccess(request: AccessRequest): boolean { |
| 51 | // Allow if any policy matches (whitelist approach) |
| 52 | return policies.some((policy) => policy(request)); |
| 53 | } |
| 54 | |
| 55 | // Middleware |
| 56 | function 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 | |
| 86 | app.get('/api/documents/:id', abacMiddleware('document', 'read'), handler); |
| 87 | app.delete('/api/documents/:id', abacMiddleware('document', 'delete'), handler); |
| Storage Method | XSS Risk | CSRF Risk | Persistence | Use Case |
|---|---|---|---|---|
| httpOnly Cookie | None | Vulnerable | Session-based | Recommended for most apps |
| localStorage | Vulnerable | None | Persistent | Avoid if possible |
| sessionStorage | Vulnerable | None | Tab-scoped | Short-lived SPA tokens |
| Memory (variable) | None | None | Lost on refresh | Most secure, least persistent |
best practice
- 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.
- 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