Authentication
WebJs doesn't ship an auth library. It provides the primitives you need to build session-based authentication cleanly. The blog example demonstrates a complete implementation using scrypt password hashing, session tokens in cookies, and middleware-based route protection.
Architecture
lib/
password.ts : hashPassword() / verifyPassword() via scrypt
session.ts : createSession() / destroySession() / getUserByToken()
plus cookie header helpers
modules/auth/
actions/
signup.server.ts : register and create session
login.server.ts : verify credentials and create session
logout.server.ts : destroy session
queries/
current-user.server.ts : read user from request cookies
app/
api/auth/
signup/route.ts : POST handler, sets Set-Cookie
login/route.ts : POST handler, sets Set-Cookie
logout/route.ts : POST handler, clears cookie
middleware.ts : rate limit on auth endpoints
dashboard/
middleware.ts : require auth, redirect to /login
Password Hashing
// lib/password.server.ts
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
export async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16);
const derived = await scryptAsync(password, salt, 64) as Buffer;
return `scrypt$${salt.toString('hex')}$${derived.toString('hex')}`;
}
export async function verifyPassword(
password: string,
stored: string | null,
): Promise<boolean> {
if (!stored) return false;
const [, saltHex, hashHex] = stored.split('$');
const salt = Buffer.from(saltHex, 'hex');
const expected = Buffer.from(hashHex, 'hex');
const derived = await scryptAsync(password, salt, expected.length) as Buffer;
return timingSafeEqual(expected, derived);
}
Session Cookies
// lib/session.server.ts
import { db } from '#db/connection.server.ts';
import { sessions } from '#db/schema.server.ts';
export const SESSION_COOKIE = 'my_session';
export async function createSession(userId: number) {
const token = randomBytes(16).toString('hex');
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await db.insert(sessions).values({ token, userId, expiresAt });
return { token, expiresAt };
}
export function sessionCookieHeader(token: string, opts = {}) {
return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`;
}
Reading the Current User
// modules/auth/queries/current-user.server.ts
'use server';
import { cookies } from '@webjsdev/server';
import { getUserByToken, SESSION_COOKIE } from '#lib/session.server.ts';
export async function currentUser() {
const token = cookies().get(SESSION_COOKIE);
return getUserByToken(token);
}
The cookies() helper from @webjsdev/server reads the in-flight Request via AsyncLocalStorage, so no parameter passing needed.
Route Protection via Middleware
// app/dashboard/middleware.ts
import { cookies } from '@webjsdev/server';
import { getUserByToken, SESSION_COOKIE } from '#lib/session.server.ts';
export default async function requireAuth(
req: Request,
next: () => Promise<Response>,
) {
const user = await getUserByToken(cookies().get(SESSION_COOKIE));
if (!user) {
const to = encodeURIComponent(new URL(req.url).pathname);
return new Response(null, {
status: 302,
headers: { location: `/login?then=${to}` },
});
}
return next();
}
This middleware only fires for routes under /dashboard/**. Unauthenticated users are redirected to /login with a return URL.
Rate Limiting Auth Endpoints
// app/api/auth/middleware.ts
import { rateLimit } from '@webjsdev/server';
export default rateLimit({ window: '10s', max: 5 });
Any request to /api/auth/** is rate-limited to 5 per 10 seconds per IP. This applies to signup, login, and logout equally.
CSRF Protection
Server actions (called via the auto-generated RPC stub) are automatically CSRF-protected by a cross-origin check: the server reads Sec-Fetch-Site (with an Origin-vs-host fallback for older browsers), and rejects a cross-site request with a 403 unless its origin is in webjs.allowedOrigins. No token or cookie is involved. See Security.
API routes (route.ts) are NOT automatically protected, since they're intended for external consumers. Authenticate a mutating route handler yourself (bearer token, API key, or an explicit origin check in middleware).
Login Form Component
The blog's <auth-forms> component demonstrates a tabbed login/signup form that POSTs to the API routes, receives a Set-Cookie session header, and redirects to the dashboard. See modules/auth/components/auth-forms.ts in the blog example for the complete implementation.