---
title: "Express"
description: "Complete auth setup for Express with ideal-auth."
---

> Documentation Index
> Fetch the complete documentation index at: https://ramonmalcolm10.github.io/ideal-auth/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

1. **Install ideal-auth and cookie-parser**

```bash
   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.

```bash title=".env"
   IDEAL_AUTH_SECRET="at-least-32-characters-long-secret-here"
```

   Generate a strong secret:

```bash
   bunx ideal-auth secret
```

---

## 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.

```ts title="src/lib/cookies.ts"
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: '/' });
},
  };
}
```

> **Caution**
>
> Express `res.cookie()` expects `maxAge` in **milliseconds**, but `ideal-auth` provides it in **seconds**. The bridge multiplies by 1000 to convert.

---

## Auth setup

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

```ts title="src/lib/auth.ts"
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

```ts title="src/app.ts"
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

```ts title="src/routes/auth.ts"
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

```ts title="src/routes/auth.ts" ins={3-46}
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

```ts title="src/routes/auth.ts" ins={3-14}
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

```ts title="src/routes/auth.ts" ins={3-17}
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.

```ts title="src/middleware/auth.ts"
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`:

```ts title="src/types/express.d.ts"
declare global {
  namespace Express {
interface Request {
  user?: {
    id: string;
    email: string;
    name: string;
  };
}
  }
}

export {};
```

---

## Protected route example

```ts title="src/routes/dashboard.ts"
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

```ts
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)

```ts title="src/middleware/optional-auth.ts"
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:

```ts
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.

### Double-submit cookie pattern

```ts title="src/middleware/csrf.ts"
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();
}
```

> **Tip**
>
> For APIs consumed by browsers, Origin header validation is the simplest and most effective CSRF defense. It works because browsers always send the `Origin` header on cross-origin requests and JavaScript cannot forge it.

### Alternative: Token-based CSRF

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

```ts title="src/middleware/csrf-token.ts"
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.

Source: https://ramonmalcolm10.github.io/ideal-auth/frameworks/express/index.mdx
