Skip to content

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.