Express
This guide walks through setting up authentication in an Express application using ideal-auth. By the end, you will have working login, registration, logout, route protection via middleware, and access to the current user in your route handlers.
Installation
Section titled “Installation”-
Install ideal-auth and cookie-parser
Terminal window bun add ideal-auth cookie-parserbun add -D @types/cookie-parser @types/expresscookie-parseris required for reading cookies from incoming requests. Express does not parse cookies by default. -
Set the session secret
Add a secret to your
.envfile. It must be at least 32 characters..env IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"Generate a strong secret:
Terminal window bunx ideal-auth secret
Cookie bridge
Section titled “Cookie bridge”Express uses req.cookies (from cookie-parser) to read cookies and res.cookie() / res.clearCookie() to write and delete them. Since the bridge needs access to both req and res, create it per-request.
import type { Request, Response } from 'express';import type { CookieBridge } from 'ideal-auth';
export function createCookieBridge(req: Request, res: Response): CookieBridge { return { get(name: string) { return req.cookies[name]; }, set(name, value, options) { res.cookie(name, value, { httpOnly: options.httpOnly, secure: options.secure, sameSite: options.sameSite, path: options.path ?? '/', ...(options.maxAge !== undefined && { maxAge: options.maxAge * 1000 }), // Express uses milliseconds }); }, delete(name) { res.clearCookie(name, { path: '/' }); }, };}Auth setup
Section titled “Auth setup”Create an auth() factory that accepts Express req and res and returns an AuthInstance.
import { createAuth, createHash } from 'ideal-auth';import { createCookieBridge } from './cookies';import { db } from './db'; // your database client
type User = { id: string; email: string; name: string; password: string;};
export const hash = createHash({ rounds: 12 });
export function auth(req: import('express').Request, res: import('express').Response) { const authFactory = createAuth<User>({ secret: process.env.IDEAL_AUTH_SECRET!, cookie: createCookieBridge(req, res), hash,
async resolveUser(id) { return db.user.findUnique({ where: { id } }); },
async resolveUserByCredentials(credentials) { return db.user.findUnique({ where: { email: credentials.email }, }); }, });
return authFactory();}App setup
Section titled “App setup”import express from 'express';import cookieParser from 'cookie-parser';import { authRouter } from './routes/auth';import { dashboardRouter } from './routes/dashboard';import { csrfProtection } from './middleware/csrf';import { requireAuth } from './middleware/auth';
const app = express();
// Parse JSON bodies and cookiesapp.use(express.json());app.use(express.urlencoded({ extended: true }));app.use(cookieParser());
// CSRF protection for state-changing requestsapp.use(csrfProtection);
// Public routesapp.use('/auth', authRouter);
// Protected routesapp.use('/dashboard', requireAuth, dashboardRouter);
app.listen(3000, () => { console.log('Server running on http://localhost:3000');});
export default app;Login route
Section titled “Login route”import { Router } from 'express';import { auth, hash } from '../lib/auth';import { db } from '../lib/db';
export const authRouter = Router();
authRouter.post('/login', async (req, res) => { try { const { email, password, remember } = req.body;
if (!email || !password) { return res.status(400).json({ error: 'Email and password are required.' }); }
const session = auth(req, res); const success = await session.attempt( { email, password }, { remember: remember ?? false }, );
if (!success) { return res.status(401).json({ error: 'Invalid email or password.' }); }
return res.json({ success: true }); } catch (error) { console.error('Login error:', error); return res.status(500).json({ error: 'Internal server error.' }); }});Registration route
Section titled “Registration route”authRouter.post('/register', async (req, res) => { try { const { email, name, password, passwordConfirmation } = req.body;
if (!email || !name || !password) { return res.status(400).json({ error: 'All fields are required.' }); }
if (password.length < 8) { return res.status(400).json({ error: 'Password must be at least 8 characters.' }); }
if (password !== passwordConfirmation) { return res.status(400).json({ error: 'Passwords do not match.' }); }
const existing = await db.user.findUnique({ where: { email } }); if (existing) { return res.status(409).json({ error: 'An account with this email already exists.' }); }
const user = await db.user.create({ data: { email, name, password: await hash.make(password), }, });
// Log the user in immediately after registration const session = auth(req, res); await session.login(user);
return res.status(201).json({ success: true }); } catch (error) { console.error('Registration error:', error); return res.status(500).json({ error: 'Internal server error.' }); }});Logout route
Section titled “Logout route”authRouter.post('/logout', async (req, res) => { try { const session = auth(req, res); await session.logout();
return res.json({ success: true }); } catch (error) { console.error('Logout error:', error); return res.status(500).json({ error: 'Internal server error.' }); }});Current user route
Section titled “Current user route”authRouter.get('/me', async (req, res) => { try { const session = auth(req, res); const user = await session.user();
if (!user) { return res.json({ user: null }); }
return res.json({ user: { id: user.id, email: user.email, name: user.name }, }); } catch (error) { console.error('User fetch error:', error); return res.status(500).json({ error: 'Internal server error.' }); }});Auth middleware
Section titled “Auth middleware”Create a reusable middleware that protects routes from unauthenticated access.
import type { Request, Response, NextFunction } from 'express';import { auth } from '../lib/auth';
export async function requireAuth(req: Request, res: Response, next: NextFunction) { try { const session = auth(req, res); const user = await session.user();
if (!user) { return res.status(401).json({ error: 'Authentication required.' }); }
// Attach user to request for downstream handlers req.user = user; next(); } catch (error) { console.error('Auth middleware error:', error); return res.status(500).json({ error: 'Internal server error.' }); }}Extend the Express Request type to include user:
declare global { namespace Express { interface Request { user?: { id: string; email: string; name: string; }; } }}
export {};Protected route example
Section titled “Protected route example”import { Router } from 'express';
export const dashboardRouter = Router();
dashboardRouter.get('/', (req, res) => { // req.user is set by the requireAuth middleware res.json({ message: `Welcome, ${req.user!.name}`, user: req.user, });});
dashboardRouter.get('/settings', (req, res) => { res.json({ message: 'Settings page', user: req.user, });});Getting the current user
Section titled “Getting the current user”In any route handler
Section titled “In any route handler”app.get('/api/profile', requireAuth, (req, res) => { // req.user is available from the middleware res.json({ user: req.user });});Optional auth (user may or may not be logged in)
Section titled “Optional auth (user may or may not be logged in)”import type { Request, Response, NextFunction } from 'express';import { auth } from '../lib/auth';
export async function optionalAuth(req: Request, res: Response, next: NextFunction) { try { const session = auth(req, res); const user = await session.user();
if (user) { req.user = { id: user.id, email: user.email, name: user.name }; }
next(); } catch { next(); }}Use it on routes where the user might be logged in but authentication is not required:
app.get('/', optionalAuth, (req, res) => { if (req.user) { res.json({ message: `Hello, ${req.user.name}` }); } else { res.json({ message: 'Hello, guest' }); }});CSRF protection
Section titled “CSRF protection”Express does not include built-in CSRF protection. Implement Origin header validation as middleware.
Double-submit cookie pattern
Section titled “Double-submit cookie pattern”import type { Request, Response, NextFunction } from 'express';
const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'];
export function csrfProtection(req: Request, res: Response, next: NextFunction) { if (SAFE_METHODS.includes(req.method)) { return next(); }
const origin = req.get('origin'); const host = req.get('host');
if (!origin || !host) { return res.status(403).json({ error: 'Forbidden: missing origin header.' }); }
try { const originHost = new URL(origin).host; if (originHost !== host) { return res.status(403).json({ error: 'Forbidden: origin mismatch.' }); } } catch { return res.status(403).json({ error: 'Forbidden: invalid origin.' }); }
next();}Alternative: Token-based CSRF
Section titled “Alternative: Token-based CSRF”If you serve HTML forms from Express (e.g., with a template engine), you can use a CSRF token approach:
import { generateToken, timingSafeEqual } from 'ideal-auth';import type { Request, Response, NextFunction } from 'express';
const CSRF_COOKIE = '_csrf';const CSRF_HEADER = 'x-csrf-token';
export function csrfToken(req: Request, res: Response, next: NextFunction) { // Generate a CSRF token and set it as a readable cookie let token = req.cookies[CSRF_COOKIE];
if (!token) { token = generateToken(32); res.cookie(CSRF_COOKIE, token, { httpOnly: false, // must be readable by JavaScript secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }); }
// Make token available for templates res.locals.csrfToken = token; next();}
export function verifyCsrfToken(req: Request, res: Response, next: NextFunction) { if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); }
const cookieToken = req.cookies[CSRF_COOKIE]; const headerToken = req.get(CSRF_HEADER) || req.body?._csrf;
if (!cookieToken || !headerToken) { return res.status(403).json({ error: 'CSRF token missing.' }); }
if (!timingSafeEqual(cookieToken, headerToken)) { return res.status(403).json({ error: 'CSRF token mismatch.' }); }
next();}Complete file structure
Section titled “Complete file structure”src/ app.ts # Express app setup lib/ auth.ts # Auth factory cookies.ts # Cookie bridge db.ts # Database client middleware/ auth.ts # requireAuth middleware csrf.ts # CSRF protection optional-auth.ts # Optional auth middleware routes/ auth.ts # Login, register, logout, me dashboard.ts # Protected dashboard routes types/ express.d.ts # Request type extensionsSecurity notes
Section titled “Security notes”- Session secret: Store
IDEAL_AUTH_SECRETin environment variables. Use a library likedotenvto load.envfiles in development. - HTTPS: Set
NODE_ENV=productionin production so session cookies are automatically markedSecure. Run Express behind a reverse proxy (nginx, Caddy) that terminates TLS. - Trust proxy: If Express is behind a reverse proxy, set
app.set('trust proxy', 1)soreq.secureandX-Forwarded-*headers work correctly. - Cookie maxAge units: Remember that Express uses milliseconds for
maxAgewhileideal-authuses seconds. The bridge in this guide handles the conversion. - cookie-parser: The
cookie-parsermiddleware is required. Without it,req.cookieswill beundefined. - Error handling: In production, avoid leaking stack traces in error responses. The
try/catchblocks in the route handlers above return generic error messages. - Rate limiting: Express is a common target for brute-force attacks. Use
ideal-auth’screateRateLimiteror a dedicated middleware likeexpress-rate-limitto protect the login endpoint.