Security
Architecture
Authentication & Authorization
at the Edge
JWT validation, session management, API keys, and RBAC patterns for Cloudflare Workers. Secure your edge applications properly.
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.