general 1. August 2024

Cloudflare Worker Auth Middleware

Lightweight Auth Middleware für Cloudflare Workers mit JWT Validation und Rate Limiting.

cloudflareworkersjwttypescriptauth

JWT Auth Middleware

Minimale JWT-Validierung für Cloudflare Workers ohne externe Dependencies.

interface Env {
  JWT_SECRET: string;
  RATE_LIMIT: KVNamespace;
}

interface JwtPayload {
  sub: string;
  exp: number;
  iat: number;
}

export async function verifyJwt(token: string, secret: string): Promise<JwtPayload | null> {
  try {
    const [headerB64, payloadB64, signatureB64] = token.split('.');

    const encoder = new TextEncoder();
    const key = await crypto.subtle.importKey(
      'raw',
      encoder.encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['verify']
    );

    const signature = Uint8Array.from(atob(signatureB64.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
    const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(`${headerB64}.${payloadB64}`));

    if (!valid) return null;

    const payload: JwtPayload = JSON.parse(atob(payloadB64));

    if (payload.exp * 1000 < Date.now()) return null;

    return payload;
  } catch {
    return null;
  }
}

export async function authMiddleware(request: Request, env: Env): Promise<Response | JwtPayload> {
  const authHeader = request.headers.get('Authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  const token = authHeader.slice(7);
  const payload = await verifyJwt(token, env.JWT_SECRET);

  if (!payload) {
    return new Response(JSON.stringify({ error: 'Invalid token' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Simple rate limiting
  const key = `rate:${payload.sub}:${Math.floor(Date.now() / 60000)}`;
  const count = parseInt(await env.RATE_LIMIT.get(key) || '0');

  if (count > 100) {
    return new Response(JSON.stringify({ error: 'Rate limited' }), {
      status: 429,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  await env.RATE_LIMIT.put(key, String(count + 1), { expirationTtl: 120 });

  return payload;
}