Authentication
Cloudwerk provides flexible patterns for implementing authentication using sessions, JWTs, or OAuth providers.
Session-Based Authentication
Section titled “Session-Based Authentication”Setting Up Session Storage
Section titled “Setting Up Session Storage”Configure KV for session storage in wrangler.toml:
[[kv_namespaces]]binding = "SESSIONS"id = "your-kv-namespace-id"Session Utilities
Section titled “Session Utilities”Create utilities for session management:
// lib/auth/session.ts
interface SessionData { userId: string; email: string; createdAt: string;}
const SESSION_DURATION = 60 * 60 * 24 * 7; // 7 days in seconds
export async function createSession( kv: KVNamespace, data: Omit<SessionData, 'createdAt'>): Promise<string> { const sessionId = crypto.randomUUID(); const session: SessionData = { ...data, createdAt: new Date().toISOString(), };
await kv.put(`session:${sessionId}`, JSON.stringify(session), { expirationTtl: SESSION_DURATION, });
return sessionId;}
export async function getSession( kv: KVNamespace, sessionId: string): Promise<SessionData | null> { const data = await kv.get(`session:${sessionId}`, 'json'); return data as SessionData | null;}
export async function destroySession( kv: KVNamespace, sessionId: string): Promise<void> { await kv.delete(`session:${sessionId}`);}Login Handler
Section titled “Login Handler”// app/api/auth/login/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { json, getContext } from '@cloudwerk/core';import { createSession } from '../../../../lib/auth/session';import { verifyPassword } from '../../../../lib/auth/password';
export async function POST(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const formData = await request.formData(); const email = formData.get('email') as string; const password = formData.get('password') as string;
// Find user const user = await env.DB .prepare('SELECT * FROM users WHERE email = ?') .bind(email) .first();
if (!user) { return json({ error: 'Invalid credentials' }, { status: 401 }); }
// Verify password const valid = await verifyPassword(password, user.password_hash); if (!valid) { return json({ error: 'Invalid credentials' }, { status: 401 }); }
// Create session const sessionId = await createSession(env.SESSIONS, { userId: user.id, email: user.email, });
// Return response with session cookie return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${60 * 60 * 24 * 7}`, }, });}Logout Handler
Section titled “Logout Handler”// app/api/auth/logout/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';import { destroySession } from '../../../../lib/auth/session';
export async function POST(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const cookie = request.headers.get('Cookie') ?? ''; const sessionId = cookie.match(/session=([^;]+)/)?.[1];
if (sessionId) { await destroySession(env.SESSIONS, sessionId); }
// Clear cookie and redirect return new Response(null, { status: 302, headers: { 'Location': '/', 'Set-Cookie': 'session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0', }, });}Auth Middleware
Section titled “Auth Middleware”Create middleware to protect routes:
// app/dashboard/middleware.tsimport type { Middleware } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';import { getSession } from '../../lib/auth/session';
export const middleware: Middleware = async (request, next) => { const ctx = getContext(); const cookie = request.headers.get('Cookie') ?? ''; const sessionId = cookie.match(/session=([^;]+)/)?.[1];
if (!sessionId) { return new Response(null, { status: 302, headers: { 'Location': '/login' }, }); }
const session = await getSession(ctx.env.SESSIONS, sessionId);
if (!session) { return new Response(null, { status: 302, headers: { 'Location': '/login', 'Set-Cookie': 'session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0', }, }); }
// Store user info for downstream handlers ctx.set('user', { id: session.userId, email: session.email });
return next();};Accessing User in Loaders
Section titled “Accessing User in Loaders”// app/dashboard/page.tsximport type { PageProps, LoaderArgs } from '@cloudwerk/core';
export async function loader({ context }: LoaderArgs) { const user = context.get('user'); const db = context.env.DB;
const { results: posts } = await db .prepare('SELECT * FROM posts WHERE author_id = ? ORDER BY created_at DESC') .bind(user.id) .all();
return { user, posts };}
export default function DashboardPage({ user, posts }: PageProps & LoaderData) { return ( <div> <h1>Welcome, {user.email}</h1> <h2>Your Posts</h2> <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> );}JWT Authentication
Section titled “JWT Authentication”Generating Tokens
Section titled “Generating Tokens”// app/api/auth/token/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { json, getContext } from '@cloudwerk/core';import { SignJWT } from 'jose';
export async function POST(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const { email, password } = await request.json();
// Authenticate user const user = await env.DB .prepare('SELECT * FROM users WHERE email = ?') .bind(email) .first();
if (!user || !(await verifyPassword(password, user.password_hash))) { return json({ error: 'Invalid credentials' }, { status: 401 }); }
// Generate JWT const secret = new TextEncoder().encode(env.JWT_SECRET); const token = await new SignJWT({ sub: user.id, email: user.email }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('24h') .sign(secret);
return json({ token, expiresIn: 86400 });}Verifying Tokens in Middleware
Section titled “Verifying Tokens in Middleware”// app/api/middleware.tsimport type { Middleware } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';import { jwtVerify } from 'jose';
export const middleware: Middleware = async (request, next) => { const ctx = getContext(); const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) { return new Response('Unauthorized', { status: 401 }); }
const token = authHeader.slice(7); const secret = new TextEncoder().encode(ctx.env.JWT_SECRET);
try { const { payload } = await jwtVerify(token, secret); ctx.set('userId', payload.sub); ctx.set('userEmail', payload.email); } catch { return new Response('Invalid token', { status: 401 }); }
return next();};OAuth / Social Login
Section titled “OAuth / Social Login”GitHub OAuth Example
Section titled “GitHub OAuth Example”// app/api/auth/github/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';
export async function GET(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext();
const authUrl = new URL('https://github.com/login/oauth/authorize'); authUrl.searchParams.set('client_id', env.GITHUB_CLIENT_ID); authUrl.searchParams.set('redirect_uri', `${env.APP_URL}/api/auth/github/callback`); authUrl.searchParams.set('scope', 'user:email');
return new Response(null, { status: 302, headers: { 'Location': authUrl.toString() }, });}// app/api/auth/github/callback/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';import { createSession } from '../../../../../lib/auth/session';
export async function GET(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const url = new URL(request.url); const code = url.searchParams.get('code');
if (!code) { return new Response(null, { status: 302, headers: { 'Location': '/login?error=oauth_failed' }, }); }
// Exchange code for access token const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ client_id: env.GITHUB_CLIENT_ID, client_secret: env.GITHUB_CLIENT_SECRET, code, }), });
const { access_token } = await tokenResponse.json();
// Get user profile const profileResponse = await fetch('https://api.github.com/user', { headers: { 'Authorization': `Bearer ${access_token}`, 'User-Agent': 'Cloudwerk-App', }, });
const profile = await profileResponse.json();
// Find or create user let user = await env.DB .prepare('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?') .bind('github', profile.id.toString()) .first();
if (!user) { const userId = crypto.randomUUID(); await env.DB .prepare('INSERT INTO users (id, email, name, oauth_provider, oauth_id) VALUES (?, ?, ?, ?, ?)') .bind(userId, profile.email, profile.name || profile.login, 'github', profile.id.toString()) .run();
user = { id: userId, email: profile.email }; }
// Create session const sessionId = await createSession(env.SESSIONS, { userId: user.id, email: user.email, });
return new Response(null, { status: 302, headers: { 'Location': '/dashboard', 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${60 * 60 * 24 * 7}`, }, });}Role-Based Access Control
Section titled “Role-Based Access Control”Define Roles
Section titled “Define Roles”// lib/auth/roles.tsexport const ROLES = { ADMIN: 'admin', MODERATOR: 'moderator', USER: 'user',} as const;
export type Role = (typeof ROLES)[keyof typeof ROLES];
export const PERMISSIONS = { [ROLES.ADMIN]: ['read', 'write', 'delete', 'admin'], [ROLES.MODERATOR]: ['read', 'write', 'delete'], [ROLES.USER]: ['read', 'write'],} as const;
export function hasPermission(role: Role, permission: string): boolean { return PERMISSIONS[role]?.includes(permission) ?? false;}Admin Middleware
Section titled “Admin Middleware”// app/admin/middleware.tsimport type { Middleware } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';import { ROLES } from '../../lib/auth/roles';
export const middleware: Middleware = async (request, next) => { const ctx = getContext(); const user = ctx.get('user');
if (!user) { return new Response('Unauthorized', { status: 401 }); }
// Fetch user role from database const userData = await ctx.env.DB .prepare('SELECT role FROM users WHERE id = ?') .bind(user.id) .first();
if (userData?.role !== ROLES.ADMIN) { return new Response('Forbidden', { status: 403 }); }
return next();};Loader-Level Authorization
Section titled “Loader-Level Authorization”// app/admin/users/page.tsximport type { PageProps, LoaderArgs } from '@cloudwerk/core';import { ROLES } from '../../../lib/auth/roles';
export async function loader({ context }: LoaderArgs) { const user = context.get('user');
// Verify role in loader as additional safety const userData = await context.env.DB .prepare('SELECT role FROM users WHERE id = ?') .bind(user.id) .first();
if (userData?.role !== ROLES.ADMIN) { throw new Error('Admin access required'); }
const { results: users } = await context.env.DB .prepare('SELECT id, email, name, role, created_at FROM users') .all();
return { users };}
export default function AdminUsersPage({ users }: PageProps & { users: User[] }) { return ( <div> <h1>User Management</h1> <table> <thead> <tr> <th>Email</th> <th>Name</th> <th>Role</th> </tr> </thead> <tbody> {users.map(user => ( <tr key={user.id}> <td>{user.email}</td> <td>{user.name}</td> <td>{user.role}</td> </tr> ))} </tbody> </table> </div> );}Security Best Practices
Section titled “Security Best Practices”Password Hashing
Section titled “Password Hashing”// lib/auth/password.ts
export async function hashPassword(password: string): Promise<string> { const encoder = new TextEncoder(); const data = encoder.encode(password);
// Use Web Crypto API available in Workers const salt = crypto.getRandomValues(new Uint8Array(16)); const keyMaterial = await crypto.subtle.importKey( 'raw', data, 'PBKDF2', false, ['deriveBits'] );
const hash = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256', }, keyMaterial, 256 );
// Combine salt and hash for storage const combined = new Uint8Array(salt.length + new Uint8Array(hash).length); combined.set(salt); combined.set(new Uint8Array(hash), salt.length);
return btoa(String.fromCharCode(...combined));}
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> { const combined = Uint8Array.from(atob(storedHash), c => c.charCodeAt(0)); const salt = combined.slice(0, 16); const originalHash = combined.slice(16);
const encoder = new TextEncoder(); const data = encoder.encode(password);
const keyMaterial = await crypto.subtle.importKey( 'raw', data, 'PBKDF2', false, ['deriveBits'] );
const hash = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256', }, keyMaterial, 256 );
const newHash = new Uint8Array(hash);
// Constant-time comparison if (newHash.length !== originalHash.length) return false; let result = 0; for (let i = 0; i < newHash.length; i++) { result |= newHash[i] ^ originalHash[i]; } return result === 0;}Rate Limiting
Section titled “Rate Limiting”// app/api/auth/login/middleware.tsimport type { Middleware } from '@cloudwerk/core';import { getContext } from '@cloudwerk/core';
export const middleware: Middleware = async (request, next) => { const ctx = getContext(); const ip = request.headers.get('CF-Connecting-IP') ?? 'unknown'; const key = `rate_limit:login:${ip}`;
// Check rate limit const attempts = parseInt(await ctx.env.CACHE.get(key) ?? '0'); if (attempts >= 5) { return new Response('Too many attempts. Try again later.', { status: 429, headers: { 'Retry-After': '900' }, }); }
// Increment counter await ctx.env.CACHE.put(key, String(attempts + 1), { expirationTtl: 900 });
return next();};Next Steps
Section titled “Next Steps”- Database Guide - Store user data
- Middleware Guide - Request middleware patterns
- Security Reference - Platform limits