Authentication
Cloudwerk provides a comprehensive authentication system through the @cloudwerk/auth package. It supports multiple providers (OAuth, credentials, email, WebAuthn), flexible session strategies, role-based access control, multi-tenancy, and rate limiting.
Quick Start
Section titled “Quick Start”-
Install the auth package:
Terminal window pnpm add @cloudwerk/auth -
Create your auth configuration:
// app/auth/config.tsimport { defineAuthConfig } from '@cloudwerk/auth/convention'export default defineAuthConfig({basePath: '/auth',session: {strategy: 'database',maxAge: 30 * 24 * 60 * 60, // 30 days},}) -
Add a provider:
// app/auth/providers/github.tsimport { defineProvider, github } from '@cloudwerk/auth/convention'export default defineProvider(github({clientId: process.env.GITHUB_CLIENT_ID!,clientSecret: process.env.GITHUB_CLIENT_SECRET!,})) -
Use auth helpers in your routes:
// app/dashboard/page.tsximport { requireAuth } from '@cloudwerk/auth'export async function loader() {const user = requireAuth() // Redirects if not logged inreturn { user }}export default function DashboardPage({ user }) {return <h1>Welcome, {user.name}</h1>}
Convention File Structure
Section titled “Convention File Structure”Auth configuration is defined using convention files in the app/auth/ directory:
Directoryapp/auth/
- config.ts # Main auth configuration
- callbacks.ts # Lifecycle callbacks
- pages.ts # Custom auth page paths
- rbac.ts # Role and permission definitions
Directoryproviders/
- github.ts # OAuth provider
- google.ts # OIDC provider
- credentials.ts # Email/password
- email.ts # Magic link
Providers
Section titled “Providers”OAuth Providers
Section titled “OAuth Providers”Cloudwerk includes pre-built OAuth providers for popular services:
// app/auth/providers/github.tsimport { defineProvider, github } from '@cloudwerk/auth/convention'
export default defineProvider( github({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, scope: 'read:user user:email', // Optional custom scope }))// app/auth/providers/google.tsimport { defineProvider, google } from '@cloudwerk/auth/convention'
export default defineProvider( google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, scope: 'openid email profile', }))// app/auth/providers/discord.tsimport { defineProvider, discord } from '@cloudwerk/auth/convention'
export default defineProvider( discord({ clientId: process.env.DISCORD_CLIENT_ID!, clientSecret: process.env.DISCORD_CLIENT_SECRET!, }))Custom OAuth Provider
Section titled “Custom OAuth Provider”Create a custom OAuth 2.0 provider:
// app/auth/providers/custom.tsimport { defineProvider, createOAuth2Provider } from '@cloudwerk/auth'
export default defineProvider( createOAuth2Provider({ id: 'custom', name: 'Custom Provider', clientId: process.env.CUSTOM_CLIENT_ID!, clientSecret: process.env.CUSTOM_CLIENT_SECRET!, authorization: 'https://auth.example.com/oauth/authorize', token: 'https://auth.example.com/oauth/token', userinfo: 'https://api.example.com/user', scope: 'read:user', profile(profile) { return { id: profile.id, email: profile.email, name: profile.name, image: profile.avatar_url, } }, }))Credentials Provider
Section titled “Credentials Provider”Email/password authentication with customizable validation:
// app/auth/providers/credentials.tsimport { defineProvider, credentials, verifyPassword } from '@cloudwerk/auth'import { db } from '@cloudwerk/core/bindings'
export default defineProvider( credentials({ credentials: { email: { label: 'Email', type: 'email', required: true, }, password: { label: 'Password', type: 'password', required: true, }, }, async authorize(creds) { const user = await db .prepare('SELECT * FROM users WHERE email = ?') .bind(creds.email) .first()
if (!user) return null
const valid = await verifyPassword(creds.password, user.password_hash) if (!valid) return null
return { id: user.id, email: user.email, name: user.name, emailVerified: user.email_verified_at ? new Date(user.email_verified_at) : null, } }, }))Email / Magic Link Provider
Section titled “Email / Magic Link Provider”Passwordless authentication via email:
// app/auth/providers/email.tsimport { defineProvider, email } from '@cloudwerk/auth'
export default defineProvider( email({ maxAge: 24 * 60 * 60, // 24 hours async sendVerificationRequest({ identifier, url }) { await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: identifier, subject: 'Sign in to MyApp', html: `<a href="${url}">Click here to sign in</a>`, }), }) }, }))WebAuthn / Passkey Provider
Section titled “WebAuthn / Passkey Provider”Modern passwordless authentication with passkeys:
// app/auth/providers/passkey.tsimport { defineProvider, passkey } from '@cloudwerk/auth'
export default defineProvider( passkey({ rpName: 'My App', rpId: 'myapp.com', origin: 'https://myapp.com', authenticatorAttachment: 'platform', userVerification: 'preferred', }))For a complete passkey implementation guide, see Passkey Setup.
Session Management
Section titled “Session Management”Session Strategies
Section titled “Session Strategies”Cloudwerk supports two session strategies:
Server-side sessions stored in Cloudflare KV:
// app/auth/config.tsimport { defineAuthConfig } from '@cloudwerk/auth/convention'
export default defineAuthConfig({ session: { strategy: 'database', maxAge: 30 * 24 * 60 * 60, // 30 days updateAge: 24 * 60 * 60, // Refresh once per day },})Add the KV binding to wrangler.toml:
[[kv_namespaces]]binding = "AUTH_SESSIONS"id = "your-kv-namespace-id"Stateless JWT sessions stored in cookies:
// app/auth/config.tsimport { defineAuthConfig } from '@cloudwerk/auth/convention'
export default defineAuthConfig({ session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, }, // Required for JWT signing secret: process.env.AUTH_SECRET,})Cookie Configuration
Section titled “Cookie Configuration”Customize session cookies:
export default defineAuthConfig({ cookies: { sessionToken: { name: '__Secure-session', options: { secure: true, httpOnly: true, sameSite: 'lax', path: '/', }, }, },})Route Protection
Section titled “Route Protection”Auth Middleware
Section titled “Auth Middleware”Protect entire route segments with middleware:
// app/dashboard/middleware.tsimport { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({ unauthenticatedRedirect: '/login',})Context Helpers
Section titled “Context Helpers”Use context helpers in loaders and handlers:
import { getUser, getSession, isAuthenticated, requireAuth,} from '@cloudwerk/auth'
// Get user (returns null if not authenticated)export async function loader() { const user = getUser() return { user }}
// Require authentication (redirects if not authenticated)export async function loader() { const user = requireAuth() return { user }}
// Require authentication with error instead of redirectexport async function GET() { const user = requireAuth({ throwError: true }) return json({ user })}
// Check authentication statusexport async function loader() { if (isAuthenticated()) { return { user: getUser() } } return { user: null }}API Route Protection
Section titled “API Route Protection”For JSON APIs, return errors instead of redirects:
// app/api/profile/route.tsimport { requireAuth } from '@cloudwerk/auth'import { json } from '@cloudwerk/core'
export async function GET() { const user = requireAuth({ throwError: true }) return json({ user })}Role-Based Access Control (RBAC)
Section titled “Role-Based Access Control (RBAC)”Defining Roles
Section titled “Defining Roles”Create role and permission definitions:
// app/auth/rbac.tsimport { defineRBAC } from '@cloudwerk/auth/convention'
export default defineRBAC({ roles: [ { id: 'admin', name: 'Administrator', permissions: ['*'], // Full access description: 'Full system access', }, { id: 'editor', name: 'Editor', permissions: [ 'posts:create', 'posts:read', 'posts:update', 'posts:delete:own', // Own resources only 'media:*', ], }, { id: 'viewer', name: 'Viewer', permissions: ['posts:read', 'media:read'], }, ], defaultRole: 'viewer', hierarchy: { editor: ['viewer'], // Editor inherits viewer permissions },})Checking Permissions
Section titled “Checking Permissions”import { hasRole, hasPermission, requireRole, requirePermission } from '@cloudwerk/auth'
// Check roleif (hasRole('admin')) { // Admin-only logic}
// Check permissionif (hasPermission('posts:delete')) { // Can delete any post}
// Require role (throws ForbiddenError if lacking)export async function DELETE(request, { params }) { requireRole('admin') await deleteUser(params.id) return json({ success: true })}
// Require permissionexport async function POST(request) { requirePermission('posts:create') // Create post...}Role Middleware
Section titled “Role Middleware”Protect routes by role:
// app/admin/middleware.tsimport { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({ role: 'admin', unauthorizedRedirect: '/forbidden',})
// Multiple roles (any of)export const middleware = authMiddleware({ roles: ['admin', 'moderator'],})Custom Authorization
Section titled “Custom Authorization”For complex authorization logic:
// app/api/posts/[id]/middleware.tsimport { authMiddleware } from '@cloudwerk/auth/middleware'
export const middleware = authMiddleware({ async authorize(user, request) { const url = new URL(request.url) const postId = url.pathname.split('/').pop()
// Check if user owns the post const post = await getPost(postId) return post.authorId === user.id || user.roles?.includes('admin') },})Multi-Tenancy
Section titled “Multi-Tenancy”Multi-tenancy allows a single application to serve multiple customers (tenants) with isolated data. Common patterns include:
- Subdomain-based:
acme.yourapp.com→ tenant “acme” - Path-based:
yourapp.com/acme/dashboard→ tenant “acme” - Header-based: Custom
X-Tenant-IDheader
The tenant resolver middleware runs before your routes, identifying the tenant from the request and making it available throughout your application.
Tenant Resolution
Section titled “Tenant Resolution”Configure how tenants are identified:
// app/middleware.tsimport { createTenantResolver, createD1TenantStorage } from '@cloudwerk/auth/tenant'
const storage = createD1TenantStorage(env.DB)const resolver = createTenantResolver(storage, { strategy: 'subdomain', baseDomain: 'myapp.com',})
export const middleware = async (request, next) => { const { tenant } = await resolver.require(request) setContext('tenant', tenant) return next()}Resolution Strategies
Section titled “Resolution Strategies”| Strategy | Example | Description |
|---|---|---|
subdomain | acme.myapp.com | Tenant from subdomain |
path | /t/acme/dashboard | Tenant from URL path |
header | X-Tenant-ID: acme | Tenant from request header |
cookie | tenant=acme | Tenant from cookie |
Rate Limiting
Section titled “Rate Limiting”Auth-Specific Rate Limiters
Section titled “Auth-Specific Rate Limiters”Protect auth endpoints from brute force attacks:
// app/api/auth/login/middleware.tsimport { createLoginRateLimiter, createFixedWindowStorage,} from '@cloudwerk/auth/rate-limit'
const storage = createFixedWindowStorage(env.RATE_LIMIT_KV)const loginLimiter = createLoginRateLimiter(storage, { limit: 5, // 5 attempts window: 900, // per 15 minutes})
export const middleware = async (request, next) => { const { response } = await loginLimiter.check(request) if (response) return response // Rate limited return next()}Pre-built Limiters
Section titled “Pre-built Limiters”| Limiter | Default | Description |
|---|---|---|
createLoginRateLimiter | 5 per 15 min | Login attempts by IP + email |
createPasswordResetRateLimiter | 3 per hour | Password reset by IP |
createEmailVerificationRateLimiter | 5 per hour | Email verification by IP |
Client-Side Auth
Section titled “Client-Side Auth”Sign In / Sign Out
Section titled “Sign In / Sign Out”import { signIn, signOut, getSession } from '@cloudwerk/auth/client'
// Sign in with providerawait signIn('github')
// Sign in with credentialsawait signIn('credentials', { password: 'password', redirectTo: '/dashboard',})
// Sign outawait signOut({ redirectTo: '/' })
// Get current sessionconst session = await getSession()if (session) { console.log('Logged in as', session.user.email)}Auth Store (for frameworks)
Section titled “Auth Store (for frameworks)”import { createAuthStore } from '@cloudwerk/auth/client'
const authStore = createAuthStore()
// Subscribe to auth state changesauthStore.subscribe((state) => { console.log('Auth state:', state.status, state.user)})
// Get current stateconst { user, status } = authStore.getState()React Component Example
Section titled “React Component Example”Here’s a complete example of using auth state in a React component:
// components/AuthStatus.tsx'use client'
import { createAuthStore, signIn, signOut } from '@cloudwerk/auth/client'import { useEffect, useState } from 'react'
const authStore = createAuthStore()
export function AuthStatus() { const [session, setSession] = useState(authStore.getSession())
useEffect(() => { // Subscribe to auth changes (e.g., after sign-in/sign-out) return authStore.subscribe((state) => { setSession(state.session) }) }, [])
if (!session) { return <button onClick={() => signIn('github')}>Sign In</button> }
return ( <div> <span>Welcome, {session.user.name}</span> <button onClick={() => signOut()}>Sign Out</button> </div> )}Auth Callbacks
Section titled “Auth Callbacks”Customize the auth flow with lifecycle callbacks:
// app/auth/callbacks.tsimport { defineAuthCallbacks } from '@cloudwerk/auth/convention'
export default defineAuthCallbacks({ async signIn({ user, account, profile }) { // Called when user signs in // Return false to deny sign-in return true },
async session({ session, user }) { // Customize session data session.user.role = user.role return session },
async jwt({ token, user }) { // Customize JWT token (jwt strategy only) if (user) { token.role = user.role } return token },})Password Utilities
Section titled “Password Utilities”import { hashPassword, verifyPassword, generateToken } from '@cloudwerk/auth'
// Hash password for storageconst hash = await hashPassword('user_password')
// Verify passwordconst isValid = await verifyPassword('user_password', hash)
// Generate secure tokenconst token = await generateToken() // 32-byte random tokenError Handling
Section titled “Error Handling”Auth errors can be caught and handled:
import { UnauthenticatedError, ForbiddenError, InvalidCredentialsError,} from '@cloudwerk/auth'
try { const user = requireAuth()} catch (error) { if (error instanceof UnauthenticatedError) { return redirect('/login') } if (error instanceof ForbiddenError) { return json({ error: 'Access denied' }, { status: 403 }) } throw error}Security Best Practices
Section titled “Security Best Practices”Next Steps
Section titled “Next Steps”- Passkey Setup - Implement passwordless passkey authentication
- Auth API Reference - Complete API documentation
- Database Guide - Store user data in D1
- Middleware - Advanced middleware patterns