Build Your Own Authentication
This page is the hand-rolled route: session-based auth built on the framework primitives, where you own the session format, the password hashing, and the route-protection rules. The blog example is a complete implementation of it, using scrypt password hashing, session tokens in cookies, and middleware-based route protection.
WebJs does ship a batteries-included option, so this is a choice rather than a necessity. Auth Providers (createAuth) gives you OAuth providers, credentials login, and JWT sessions out of the box. Take that page if you want OAuth or do not want to own the session. Stay here if you want full control over how a session is minted, stored, and checked.
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 are automatically CSRF-protected by a cross-origin check, whether called via the auto-generated RPC stub or submitted through a bound <form>: 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.