Security Architecture

Authentication & Authorization
at the Edge

JWT validation, session management, API keys, and RBAC patterns for Cloudflare Workers. Secure your edge applications properly.

๐Ÿ“– 15 min read January 24, 2026

Authentication at the edge is different. No persistent connections. No server state. Every request must be validated independently, but you can't afford database round-trips on every call.

Here's how we secure 28 Workers handling sensitive data with sub-50ms auth overhead.

Edge Authentication Flow
๐Ÿ”‘ Extract Token
โ†’
โœ“ Verify Signature
โ†’
โฑ๏ธ Check Expiry
โ†’
๐Ÿ›ก๏ธ Validate Claims
โ†’
โœ… Authorize

Pattern 1: JWT Validation

Validate JWTs without external calls using Web Crypto API:

jwt-validator.ts
interface JWTPayload { sub: string; email: string; roles: string[]; exp: number; iat: number; iss: string; } async function verifyJWT( token: string, env: Env ): Promise<JWTPayload> { const [headerB64, payloadB64, signatureB64] = token.split('.'); // Decode payload const payload: JWTPayload = JSON.parse( atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')) ); // Check expiration if (payload.exp < Date.now() / 1000) { throw new AuthError('TOKEN_EXPIRED'); } // Verify issuer if (payload.iss !== env.JWT_ISSUER) { throw new AuthError('INVALID_ISSUER'); } // Get signing key (cached) const key = await getSigningKey(env); // Verify signature const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); const signature = base64UrlDecode(signatureB64); const valid = await crypto.subtle.verify( { name: 'RSASSA-PKCS1-v1_5' }, key, signature, data ); if (!valid) { throw new AuthError('INVALID_SIGNATURE'); } return payload; } // Cache JWKS in KV async function getSigningKey(env: Env): Promise<CryptoKey> { const cached = await env.KV.get('jwks:signing-key', 'json'); if (cached) { return await crypto.subtle.importKey( 'jwk', cached, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify'] ); } // Fetch and cache JWKS const jwks = await fetch(env.JWKS_URL).then(r => r.json()); await env.KV.put('jwks:signing-key', JSON.stringify(jwks.keys[0]), { expirationTtl: 3600 // 1 hour }); return await crypto.subtle.importKey(...); }

Pattern 2: API Key Authentication

For service-to-service or public APIs, use hashed API keys:

api-key-auth.ts
interface APIKeyRecord { hash: string; tenant_id: string; permissions: string[]; rate_limit: number; created_at: string; } async function validateAPIKey( request: Request, env: Env ): Promise<APIKeyRecord> { const apiKey = request.headers.get('X-API-Key') || new URL(request.url).searchParams.get('api_key'); if (!apiKey) { throw new AuthError('API_KEY_REQUIRED'); } // Hash the key (never store plain text) const hash = await hashAPIKey(apiKey); // Look up in KV (cached from DB) const record = await env.KV.get(`apikey:${hash}`, 'json') as APIKeyRecord; if (!record) { throw new AuthError('INVALID_API_KEY'); } return record; } async function hashAPIKey(key: string): Promise<string> { const data = new TextEncoder().encode(key); const hash = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')) .join(''); }

Pattern 3: Role-Based Access Control

rbac.ts
const PERMISSIONS = { admin: ['read', 'write', 'delete', 'admin'], editor: ['read', 'write'], viewer: ['read'], api: ['read', 'api'] }; function authorize( user: JWTPayload, resource: string, action: string ): boolean { // Collect all permissions from user's roles const userPermissions = new Set<string>(); for (const role of user.roles) { const perms = PERMISSIONS[role] || []; perms.forEach(p => userPermissions.add(p)); } return userPermissions.has(action); } // Middleware usage function requirePermission(action: string) { return async (request: Request, env: Env, ctx: Context) => { const user = ctx.user; // Set by auth middleware if (!authorize(user, ctx.resource, action)) { throw new AuthError('FORBIDDEN', 403); } }; }

Pattern 4: Session Management with KV

sessions.ts
interface Session { id: string; user_id: string; data: Record<string, any>; created_at: number; expires_at: number; } async function createSession( userId: string, data: Record<string, any>, env: Env ): Promise<Session> { const session: Session = { id: crypto.randomUUID(), user_id: userId, data, created_at: Date.now(), expires_at: Date.now() + (24 * 60 * 60 * 1000) // 24h }; await env.KV.put( `session:${session.id}`, JSON.stringify(session), { expirationTtl: 86400 } ); return session; } async function getSession( sessionId: string, env: Env ): Promise<Session | null> { const session = await env.KV.get(`session:${sessionId}`, 'json'); if (!session || session.expires_at < Date.now()) { return null; } return session as Session; } async function revokeSession(sessionId: string, env: Env) { await env.KV.delete(`session:${sessionId}`); }
Security Tip
Never store sensitive data in JWTsโ€”they're base64 encoded, not encrypted. Use JWTs for identity and short-lived access. Store sensitive session data server-side in KV.

Auth Middleware Pattern

auth-middleware.ts
export function withAuth(handler: Handler): Handler { return async (request, env, ctx) => { const authHeader = request.headers.get('Authorization'); // Try JWT Bearer token if (authHeader?.startsWith('Bearer ')) { const token = authHeader.slice(7); ctx.user = await verifyJWT(token, env); ctx.authMethod = 'jwt'; } // Try API Key else if (request.headers.has('X-API-Key')) { const apiKey = await validateAPIKey(request, env); ctx.tenant = apiKey.tenant_id; ctx.permissions = apiKey.permissions; ctx.authMethod = 'api_key'; } // Try session cookie else { const sessionId = getCookie(request, 'session_id'); if (sessionId) { const session = await getSession(sessionId, env); if (session) { ctx.user = session.data.user; ctx.session = session; ctx.authMethod = 'session'; } } } if (!ctx.user && !ctx.tenant) { throw new AuthError('UNAUTHORIZED', 401); } return handler(request, env, ctx); }; }

Security Checklist

  • Never store API keys in plain textโ€”always hash
  • Cache JWKS with short TTL for key rotation
  • Validate all JWT claims (exp, iss, aud)
  • Use constant-time comparison for secrets
  • Implement rate limiting per auth identity
  • Log all authentication failures
  • Set secure cookie flags (HttpOnly, Secure, SameSite)
  • Rotate signing keys periodically

Edge authentication must be fast and stateless. Cache everything possible, but never compromise on signature verification or claim validation.

Related Articles

API Gateway Patterns
Read more โ†’
Multi-Tenant SaaS
Read more โ†’
Error Handling Patterns
Read more โ†’

Need Secure Edge Systems?

We build authentication infrastructure that scales.

โ†’ Get Started