Skip to content

Authentication

Cloudwerk provides flexible patterns for implementing authentication using sessions, JWTs, or OAuth providers.

Configure KV for session storage in wrangler.toml:

[[kv_namespaces]]
binding = "SESSIONS"
id = "your-kv-namespace-id"

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}`);
}
// app/api/auth/login/route.ts
import 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}`,
},
});
}
// app/api/auth/logout/route.ts
import 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',
},
});
}

Create middleware to protect routes:

// app/dashboard/middleware.ts
import 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();
};
// app/dashboard/page.tsx
import 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>
);
}
// app/api/auth/token/route.ts
import 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 });
}
// app/api/middleware.ts
import 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();
};
// app/api/auth/github/route.ts
import 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.ts
import 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}`,
},
});
}
// lib/auth/roles.ts
export 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;
}
// app/admin/middleware.ts
import 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();
};
// app/admin/users/page.tsx
import 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>
);
}
// 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;
}
// app/api/auth/login/middleware.ts
import 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();
};