Skip to content

Express

Complete auth setup for Express with ideal-auth.

Updated View as Markdown

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

  1. Install ideal-auth and cookie-parser

    bun add ideal-auth cookie-parser
    bun add -D @types/cookie-parser @types/express

    cookie-parser is required for reading cookies from incoming requests. Express does not parse cookies by default.

  2. Set the session secret

    Add a secret to your .env file. It must be at least 32 characters.

    .envbash
    IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"

    Generate a strong secret:

    bunx ideal-auth secret

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.

src/lib/cookies.tsts
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

Create an auth() factory that accepts Express req and res and returns an AuthInstance.

src/lib/auth.tsts
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

src/app.tsts
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 cookies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

// CSRF protection for state-changing requests
app.use(csrfProtection);

// Public routes
app.use('/auth', authRouter);

// Protected routes
app.use('/dashboard', requireAuth, dashboardRouter);

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

export default app;

Login route

src/routes/auth.tsts
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

src/routes/auth.tsts
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

src/routes/auth.tsts
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

src/routes/auth.tsts
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

Create a reusable middleware that protects routes from unauthenticated access.

src/middleware/auth.tsts
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:

src/types/express.d.tsts
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        name: string;
      };
    }
  }
}

export {};

Protected route example

src/routes/dashboard.tsts
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

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)

src/middleware/optional-auth.tsts
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

Express does not include built-in CSRF protection. Implement Origin header validation as middleware.

src/middleware/csrf.tsts
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

If you serve HTML forms from Express (e.g., with a template engine), you can use a CSRF token approach:

src/middleware/csrf-token.tsts
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

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 extensions

Security notes

  • Session secret: Store IDEAL_AUTH_SECRET in environment variables. Use a library like dotenv to load .env files in development.
  • HTTPS: Set NODE_ENV=production in production so session cookies are automatically marked Secure. 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) so req.secure and X-Forwarded-* headers work correctly.
  • Cookie maxAge units: Remember that Express uses milliseconds for maxAge while ideal-auth uses seconds. The bridge in this guide handles the conversion.
  • cookie-parser: The cookie-parser middleware is required. Without it, req.cookies will be undefined.
  • Error handling: In production, avoid leaking stack traces in error responses. The try/catch blocks in the route handlers above return generic error messages.
  • Rate limiting: Express is a common target for brute-force attacks. Use ideal-auth’s createRateLimiter or a dedicated middleware like express-rate-limit to protect the login endpoint.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close