---
title: "Configuration"
description: "All configuration options for 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.

# Configuration

import SecretGenerator from '../../components/SecretGenerator.astro';

## Environment variables

| Variable | Description | Required |
|---|---|---|
| `IDEAL_AUTH_SECRET` | Encryption secret for sessions and token signing. Must be at least 32 characters. Generate with `bunx ideal-auth secret`. | Yes |
| `ENCRYPTION_KEY` | Encryption key for data at rest (TOTP secrets, access tokens). Generate with `bunx ideal-auth encryption-key`. | Only if using `encrypt()`/`decrypt()` |

Or use the CLI:

```bash
bunx ideal-auth secret          # generates IDEAL_AUTH_SECRET
bunx ideal-auth encryption-key  # generates ENCRYPTION_KEY
```

:::caution
Store secrets in environment variables. Never hard-code them or commit them to version control. Use separate keys for separate purposes — `IDEAL_AUTH_SECRET` for sessions, `ENCRYPTION_KEY` for data at rest, `TRANSFER_TOKEN_SECRET` for multi-tenant transfer tokens.
:::

---

## `createAuth`

`createAuth` is the main entry point for session authentication. It returns a factory function that produces a request-scoped `AuthInstance`.

```ts
import { createAuth, createHash } from 'ideal-auth';

const hash = createHash();

const auth = createAuth({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  session: {
cookieName: 'my_session',
maxAge: 60 * 60 * 24, // 1 day
rememberMaxAge: 60 * 60 * 24 * 90, // 90 days
cookie: {
  sameSite: 'strict',
  domain: '.example.com',
},
  },
  resolveUser: async (id) => db.user.findUnique({ where: { id } }),
  resolveUserByCredentials: async (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
  hash,
  credentialKey: 'password',
  passwordField: 'password',
});
```

### Config options

| Option | Type | Default | Description |
|---|---|---|---|
| `secret` | `string` | — | **Required.** Encryption secret, at least 32 characters. Typically `process.env.IDEAL_AUTH_SECRET`. |
| `cookie` | `CookieBridge` | — | **Required.** Object with `get`, `set`, and `delete` methods for your framework's cookie API. See [Getting Started](/ideal-auth/ideal-auth/getting-started/). |
| `session` | `object` | See below | Session configuration (cookie name, TTL, cookie options). |
| `resolveUser` | `(id: string) => Promise<TUser \| null \| undefined>` | — | Looks up a user by ID. Called when rehydrating a session from an encrypted cookie. **Required** unless `sessionFields` is provided. The return type defines what `user()` exposes — only select safe fields (no password). |
| `sessionFields` | `(keyof TUser & string)[]` | — | Fields from the user object to store in the session cookie. When provided, `user()` returns only `id` + the declared fields. **Required** unless `resolveUser` is provided. Cannot be used together with `resolveUser`. |
| `resolveUserByCredentials` | `(credentials) => Promise<AnyUser \| null \| undefined>` | `undefined` | Looks up a user by credentials (with the password field stripped). Used by `attempt()` in the Laravel-style flow. Can return any shape — only needs `id` + the `passwordField` for verification. Does not need to match `TUser`. |
| `hash` | `HashInstance` | `undefined` | A `createHash()` instance. Required if using `resolveUserByCredentials` with `attempt()`. |
| `credentialKey` | `string` | `'password'` | The key in the credentials object that holds the plaintext password. `attempt()` strips this key before passing credentials to `resolveUserByCredentials`. |
| `passwordField` | `string` | `'password'` | The field on the user object that holds the stored bcrypt hash. Used by `attempt()` to verify the password. |
| `attemptUser` | `(credentials: Record<string, any>) => Promise<TUser \| null>` | `undefined` | Escape hatch for full control over credential verification. If provided, `attempt()` delegates entirely to this function instead of using `hash` + `resolveUserByCredentials`. |

### `CookieBridge` interface

The cookie bridge decouples ideal-auth from any specific framework. It has three methods:

```ts
interface CookieBridge {
  get(name: string): Promise<string | undefined> | string | undefined;
  set(name: string, value: string, options: CookieOptions): Promise<void> | void;
  delete(name: string): Promise<void> | void;
}
```

All three methods can be synchronous or asynchronous.

### `attempt()` strategies

There are two ways to configure `attempt()`:

**Laravel-style (recommended):** Provide `hash`, `resolveUserByCredentials`, and optionally `credentialKey` and `passwordField`. The `attempt()` method will strip the password from credentials, look up the user, and verify the hash automatically.

```ts
const auth = createAuth({
  // ...
  hash: createHash(),
  resolveUserByCredentials: async (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
  credentialKey: 'password',   // default
  passwordField: 'password',   // default
});

// Usage: auth().attempt({ email, password })
```

**Escape hatch:** Provide `attemptUser` for complete control. This takes precedence over the Laravel-style config.

```ts
const auth = createAuth({
  // ...
  attemptUser: async (credentials) => {
const user = await db.user.findUnique({
  where: { email: credentials.email },
});
if (!user) return null;
const valid = await customVerify(credentials.password, user.hash);
return valid ? user : null;
  },
});
```

### Session modes

`createAuth` supports two mutually exclusive modes for how `user()` resolves the authenticated user. Provide exactly one of `resolveUser` or `sessionFields`. TypeScript will show an error in your editor if you provide both or neither.

**Database-backed (`resolveUser`):** The session cookie stores only the user ID. Every `user()` call invokes `resolveUser(id)` to fetch the user from your database. The return type of `resolveUser` defines what `user()` exposes — only select safe fields.

```ts
// TUser is the safe type — no password, no sensitive fields
type SafeUser = {
  id: string;
  email: string;
  name: string;
  role: string;
};

const auth = createAuth<SafeUser>({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,

  // Returns SafeUser — this is what user() exposes
  resolveUser: async (id) => db.user.findFirst({
where: { id },
columns: { id: true, email: true, name: true, role: true }, // no password
  }),

  // Can return any shape — only needs id + password field for verification
  // Does NOT need to match SafeUser
  resolveUserByCredentials: async (creds) => db.user.findFirst({
where: { email: creds.email },
  }),
  hash,
});

const user = await auth().user();
// Type: SafeUser | null — no password, safe to pass to client
```

**Cookie-backed (`sessionFields`):** The session cookie stores the user ID plus the declared fields. `user()` returns `TUser | null`. Since `TUser` should not include sensitive fields like password, the return type is already safe.

```ts
// TUser is the session user — no password
type SessionUser = {
  id: string;
  email: string;
  name: string;
  role: string;
};

const auth = createAuth<SessionUser>({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  sessionFields: ['email', 'name', 'role'],
  // Can return any shape — only needs id + password field for verification
  resolveUserByCredentials: async (creds) =>
db.user.findFirst({ where: { email: creds.email } }),
  hash,
});

const user = await auth().user();
// Type: SessionUser | null — no password, safe to pass to client
```

:::note
The `id` field is always stored in the session cookie regardless of mode. You do not need to include it in `sessionFields`. If you include it, it will be silently ignored.
:::

:::caution
`loginById()` is not supported with `sessionFields` and will throw an error. It requires `resolveUser` to look up the full user object so the declared fields can be populated. Use `login(user)` instead — pass the full user object directly.
:::

:::caution
With `sessionFields`, the cookie data is a snapshot from login time. If the user updates their profile, the cookie still has the old values until they re-login. To refresh, call `auth().login(updatedUser)` after the update.
:::

### Password safety

ideal-auth strips the password field from the cached user after `attempt()` succeeds. This means even on the same request, `user()` will not include the password. However, you should still follow these practices:

- **`resolveUser` mode:** Type `TUser` as the safe type (without password). Only select non-sensitive columns in your `resolveUser` query. `resolveUserByCredentials` can return the full database row — it's only used internally for hash verification.
- **`sessionFields` mode:** Only declare non-sensitive fields. The `password` field is never stored in the cookie and is excluded from the `user()` return type.
- **Same-request `user()` after `attempt()`:** If `resolveUserByCredentials` returns fewer fields than `resolveUser` (e.g., only `id` + `email` + `password` for verification), calling `user()` on the same request after `attempt()` returns those fields minus the password. Fields like `name` or `role` will be missing until the next request, when `resolveUser` is called. To avoid this, either return the same fields from both callbacks, or don't call `user()` on the same request as `attempt()` (redirect after login instead).
- **Never pass `user()` directly to the client without filtering.** Even with correct types, always be explicit about what you send:

```ts
  // Server Component (Next.js)
  const user = await auth().user();

  // Only pass what the client needs — never spread the full user
  return <Dashboard user={{ name: user.name, email: user.email }} />;
```

| | `resolveUser` | `sessionFields` |
|---|---|---|
| `user()` reads from | Database | Cookie |
| DB calls after login | Every request | Zero |
| Data freshness | Always current | Snapshot at login |
| `loginById()` | Supported | Not supported (throws) |
| Cookie size | Minimal (~100 bytes) | Depends on stored fields (~4KB limit) |
| Best for | Real-time role/permission changes | Performance, stateless, no-DB tenants |

---

## Session config

The `session` object on `createAuth` controls cookie naming, TTL, and cookie attributes.

| Option | Type | Default | Description |
|---|---|---|---|
| `cookieName` | `string` | `'ideal_session'` | Name of the session cookie. |
| `maxAge` | `number` | `604800` (7 days) | Session lifetime in seconds. Used when `remember` is not set or `remember: false`. |
| `rememberMaxAge` | `number` | `2592000` (30 days) | Extended session lifetime in seconds. Used when `login(user, { remember: true })` is called. |
| `cookie` | `Partial<ConfigurableCookieOptions>` | `{}` | Additional cookie attributes (see table below). |
| `autoTouch` | `boolean` | `false` | Automatically extend the session when `check()`, `user()`, or `id()` is called and the session is past the halfway point. Enable for frameworks where every route can write cookies (Express, Hono). Leave disabled for Next.js (Server Components can't write cookies). |

### Cookie options

The `session.cookie` object accepts any cookie attribute except `httpOnly`, which is **always forced to `true`** and cannot be overridden.

| Option | Type | Default | Description |
|---|---|---|---|
| `secure` | `boolean` | `true` in production | Whether the cookie is sent only over HTTPS. Defaults to `true` when `NODE_ENV === 'production'`. Can be overridden. |
| `sameSite` | `'lax' \| 'strict' \| 'none'` | `'lax'` | SameSite attribute for CSRF protection. |
| `path` | `string` | `'/'` | URL path scope for the cookie. |
| `domain` | `string` | `undefined` | Domain scope for the cookie. |

:::note
`httpOnly` is excluded from `ConfigurableCookieOptions` and is always set to `true` internally. This prevents client-side JavaScript from accessing the session cookie.
:::

### Session payload

The encrypted session cookie contains a `SessionPayload`:

```ts
interface SessionPayload {
  uid: string;                       // User ID (always stored as string)
  iat: number;                       // Issued-at timestamp (Unix seconds)
  exp: number;                       // Expiration timestamp (Unix seconds)
  data?: Record<string, unknown>;    // Additional fields (when using sessionFields)
}
```

The `data` field is populated when `sessionFields` is configured. It contains only the fields you declared — nothing else from the user object leaks into the cookie.

The payload is encrypted using iron-session (AES-256-CBC + HMAC-SHA-256). It cannot be read or tampered with on the client.

:::caution
Session cookies have a **~4KB size limit** (including encryption overhead). Basic fields like `id`, `email`, `name`, and `role` fit easily. Avoid storing large values like JWTs with many claims. If you need to store an access token, test that the total cookie size stays under 4KB.
:::

### Session extension

Session cookies have a fixed expiry set at login time. There are two ways to extend sessions for active users:

#### Option 1: `autoTouch` (automatic)

Enable `autoTouch` for frameworks where every route can write cookies (Express, Hono, Elysia). When `check()`, `user()`, or `id()` is called and the session is past the halfway point of its lifetime, the cookie is automatically re-sealed with a fresh expiry.

**Global (config level)** — every `auth()` call has autoTouch enabled:

```ts
const auth = createAuth<User>({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  session: { autoTouch: true },
  resolveUser: async (id) => db.user.findFirst({ where: { id } }),
});
```

**Per-request** — pass `autoTouch` when calling `auth()`. Overrides the config default. Ideal for Next.js where middleware can write cookies but Server Components can't:

```ts
// Next.js middleware — opt in to autoTouch for this request
const session = auth({ autoTouch: true });
await session.check(); // auto-extends if past halfway

// Next.js Server Component — default, read-only
const session = auth();
await session.check(); // no cookie writes, safe in Server Components
```

:::caution
Do not enable `autoTouch` globally in Next.js — `cookies().set()` throws in Server Components. Use per-request `auth({ autoTouch: true })` in middleware only.
:::

#### Option 2: `touch()` (manual)

Call `touch()` explicitly in middleware or route handlers. When `autoTouch` is disabled (default), `touch()` only re-seals past the halfway point — safe to call on every request without performance concerns. When `autoTouch` is enabled, `touch()` re-seals immediately.

```ts
// Next.js middleware or Express/Hono route handler
const session = auth();
if (await session.check()) {
  await session.touch();
}
```

#### Behavior summary

| | `autoTouch: false` (default) | `autoTouch: true` |
|---|---|---|
| `check()` / `user()` / `id()` | Read-only, no cookie writes | Auto-reseals past halfway |
| `touch()` | Reseals past halfway only | Reseals immediately |
| Best for | Next.js (Server Components can't write cookies) | Express, Hono, Elysia, SvelteKit |

`touch()` preserves the original `iat` (issued-at) — `passwordChangedAt` invalidation continues to work after a touch. The session's `maxAge` is also preserved: remember-me sessions extend by 30 days, standard sessions by 7 days.

:::note
`touch()` only extends the session lifetime — it does not update user data in the cookie. If you need to update user data (e.g., after email verification or profile changes), call `auth().login(updatedUser)` instead.
:::

---

## Password hashing

Password hashing is **only needed when you verify credentials yourself** — specifically when using `attempt()` with the Laravel-style `hash` + `resolveUserByCredentials` flow. You do **not** need a `HashInstance` or `bcryptjs` if you:

- Use `attemptUser` (the escape hatch) — you handle verification in the callback
- Use `login(user)` directly — user is already authenticated (e.g., via OAuth, cross-domain transfer token, or an external API)
- Use `sessionFields` without any credential verification

ideal-auth accepts any object that implements the `HashInstance` interface:

```ts
interface HashInstance {
  make(password: string): Promise<string>;
  verify(password: string, hash: string): Promise<boolean>;
}
```

You can use the built-in `createHash()` (bcryptjs) or bring your own implementation.

### `createHash` (bcryptjs)

Configures bcrypt password hashing via `bcryptjs`. Automatically applies SHA-256 prehash for passwords exceeding 72 bytes (bcrypt's input limit).

:::note
`bcryptjs` is an optional peer dependency. Install it if you use `createHash()`:

```bash
bun add bcryptjs
```

If `bcryptjs` is not installed and you call `createHash()`, it will throw a clear error with installation instructions.
:::

```ts
import { createHash } from 'ideal-auth';

const hash = createHash({ rounds: 14 });

const hashed = await hash.make('my-password');
const valid = await hash.verify('my-password', hashed);
```

| Option | Type | Default | Description |
|---|---|---|---|
| `rounds` | `number` | `12` | bcrypt cost factor (salt rounds). Higher values are slower but more secure. Each increment roughly doubles the computation time. |

:::tip
The default of 12 rounds is a good balance for most applications. Increase to 14 for high-security applications. Avoid going below 10.
:::

### Custom hash (bring your own)

If your runtime provides built-in password hashing (e.g., Bun) or you prefer a different algorithm (e.g., argon2), pass your own `HashInstance` instead of using `createHash()`. No `bcryptjs` dependency needed.

### Bun (bcrypt)

```ts
import { prehash } from 'ideal-auth';
import type { HashInstance } from 'ideal-auth';

const hash: HashInstance = {
  make: (password) => Bun.password.hash(prehash(password), {
    algorithm: 'bcrypt',
    cost: 12,
  }),
  verify: (password, hash) => Bun.password.verify(prehash(password), hash),
};
```

`prehash` applies SHA-256 for passwords exceeding bcrypt's 72-byte input limit, preventing silent truncation. It returns the password unchanged if it's within the limit.
### Bun (argon2id)

```ts
import type { HashInstance } from 'ideal-auth';

const hash: HashInstance = {
  make: (password) => Bun.password.hash(password, {
    algorithm: 'argon2id',
    memoryCost: 65536, // 64 MB
    timeCost: 2,
  }),
  verify: (password, hash) => Bun.password.verify(password, hash),
};
```
### Node (argon2)

```ts
import argon2 from 'argon2';
import type { HashInstance } from 'ideal-auth';

const hash: HashInstance = {
  make: (password) => argon2.hash(password),
  verify: (password, hash) => argon2.verify(hash, password),
};
```

Then pass it to `createAuth`:

```ts
const auth = createAuth({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  resolveUser: async (id) => db.user.findUnique({ where: { id } }),
  hash, // your custom HashInstance
  resolveUserByCredentials: async (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
});
```

:::caution
When using a custom hash, you are responsible for security hardening (cost factors, algorithm choice, input validation). For custom bcrypt implementations, use `prehash()` from ideal-auth to prevent silent truncation at 72 bytes. Argon2 has no input length limit — prehash is not needed.
:::

---

## `createTokenVerifier`

Creates signed, expiring tokens for flows like password reset, email verification, and magic links. Tokens are HMAC-signed and contain the user ID and timestamps.

```ts
import { createTokenVerifier } from 'ideal-auth';

const verifier = createTokenVerifier({
  secret: process.env.IDEAL_AUTH_SECRET!,
  purpose: 'password-reset',
  expiryMs: 1000 * 60 * 15, // 15 minutes
});

const token = verifier.createToken(userId);
const result = verifier.verifyToken(token);
// result: { userId: string, iatMs: number } | null
```

### Config options

| Option | Type | Default | Description |
|---|---|---|---|
| `secret` | `string` | — | **Required.** Signing secret, at least 32 characters. |
| `purpose` | `string` | — | **Required.** Label for the flow (e.g. `'password-reset'`, `'email-verification'`, `'magic-link'`). Mixed into the HMAC signature, so a token only verifies against a verifier with the same purpose — reuse one `IDEAL_AUTH_SECRET` across flows with distinct purposes. |
| `expiryMs` | `number` | `3600000` (1 hour) | Token lifetime in milliseconds. |

### Return value of `verifyToken`

Returns `null` if the token is invalid or expired. Otherwise returns:

| Field | Type | Description |
|---|---|---|
| `userId` | `string` | The user ID embedded in the token. |
| `iatMs` | `number` | Issued-at time in **milliseconds** (not seconds). Useful for single-use checks (e.g., reject if token was issued before the last password change). |

---

## `createTOTP`

Creates a TOTP (Time-based One-Time Password) instance compliant with [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238). Used for two-factor authentication.

```ts
import { createTOTP } from 'ideal-auth';

const totp = createTOTP({
  digits: 6,
  period: 30,
  window: 1,
});

const secret = totp.generateSecret();
const uri = totp.generateQrUri({
  secret,
  issuer: 'MyApp',
  account: 'user@example.com',
});

// Pass the persisted counter from the last successful verification,
// or null if the user has never verified a code.
const { valid, counter } = totp.verify('123456', secret, null);
// On success, persist `counter` (e.g. user.totpLastUsedCounter) and
// pass it back next time — codes at or before it are rejected.
```

### Config options

| Option | Type | Default | Description |
|---|---|---|---|
| `digits` | `number` | `6` | Number of digits in the generated code. |
| `period` | `number` | `30` | Time step in seconds. A new code is generated every `period` seconds. |
| `window` | `number` | `1` | Number of time steps to check before and after the current step. A window of `1` accepts codes from the previous, current, and next period (90-second effective window with a 30-second period). |

### Methods

| Method | Returns | Description |
|---|---|---|
| `generateSecret()` | `string` | Generates a random Base32-encoded secret (20 bytes). |
| `generateQrUri({ secret, issuer, account })` | `string` | Returns an `otpauth://` URI suitable for QR code generation. |
| `verify(token, secret, lastUsedCounter)` | `{ valid: boolean; counter: number \| null }` | Verifies a TOTP code against the secret using timing-safe comparison, with built-in replay protection. Codes at or before `lastUsedCounter` are rejected. On success, persist the returned `counter` and pass it back on the next verification. Tokens are normalized: whitespace is stripped, and anything that is not exactly `digits` digits is rejected. |

---

## `generateRecoveryCodes`

Generates a set of hashed recovery codes for 2FA backup. Returns both the plaintext codes (to display to the user once) and the hashed versions (to store in your database).

```ts
import { createHash, generateRecoveryCodes, verifyRecoveryCode } from 'ideal-auth';

const hash = createHash();

// Generate codes
const { codes, hashed } = await generateRecoveryCodes(hash, 8);
// codes:  ['a1b2c3d4-e5f6g7h8', ...] — show to user
// hashed: ['$2a$12$...', ...]         — store in database

// Verify a code
const result = await verifyRecoveryCode(userInput, storedHashes, hash);
// result: { valid: boolean, remaining: string[] }
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `hashInstance` | `HashInstance` | — | **Required.** A `createHash()` instance for hashing the codes. |
| `count` | `number` | `8` | Number of recovery codes to generate. |

---

## `createRateLimiter`

Creates a rate limiter with a sliding window. Useful for protecting login endpoints from brute-force attacks.

```ts
import { createRateLimiter } from 'ideal-auth';

const limiter = createRateLimiter({
  maxAttempts: 5,
  windowMs: 15 * 60 * 1000, // 15 minutes
});

const result = await limiter.attempt('login:user@example.com');
// result: { allowed: boolean, remaining: number, resetAt: Date }

if (!result.allowed) {
  throw new Error('Too many attempts. Try again later.');
}
```

### Config options

| Option | Type | Default | Description |
|---|---|---|---|
| `maxAttempts` | `number` | — | **Required.** Maximum number of attempts allowed within the window. |
| `windowMs` | `number` | — | **Required.** Time window in milliseconds. The counter resets after this period. |
| `store` | `RateLimitStore` | `MemoryRateLimitStore` | Pluggable storage backend. Defaults to an in-memory store. Implement the `RateLimitStore` interface for Redis or database-backed stores. |

### Custom store interface

```ts
interface RateLimitStore {
  increment(key: string, windowMs: number): Promise<{ count: number; resetAt: Date }>;
  reset(key: string): Promise<void>;
}
```

### Methods

| Method | Returns | Description |
|---|---|---|
| `attempt(key)` | `Promise<RateLimitResult>` | Increments the counter for the given key and returns whether the attempt is allowed. |
| `reset(key)` | `Promise<void>` | Resets the counter for the given key (e.g., after a successful login). |

---

## Crypto utilities

Low-level cryptographic functions exported for use in custom flows.

```ts
import {
  encrypt,
  decrypt,
  signData,
  verifySignature,
  timingSafeEqual,
  generateToken,
} from 'ideal-auth';
```

| Function | Signature | Description |
|---|---|---|
| `encrypt(data, secret)` | `(string, string) => string` | Encrypts a string using the provided secret. |
| `decrypt(data, secret)` | `(string, string) => string` | Decrypts a string previously encrypted with `encrypt`. |
| `signData(data, secret)` | `(string, string) => string` | Creates an HMAC signature for the given data. |
| `verifySignature(data, signature, secret)` | `(string, string, string) => boolean` | Verifies an HMAC signature. |
| `timingSafeEqual(a, b)` | `(string, string) => boolean` | Constant-time string comparison to prevent timing attacks. |
| `generateToken(bytes?)` | `(number?) => string` | Generates a cryptographically random hex token. Default is 32 bytes (64 hex characters). |

---

## Full configuration examples

### Database-backed

```ts title="lib/auth.ts"
import {
  createAuth,
  createHash,
  createTokenVerifier,
  createTOTP,
  createRateLimiter,
} from 'ideal-auth';
import { cookies } from 'next/headers';
import { db } from './db';

// Cookie bridge (Next.js App Router)
const cookieBridge = {
  async get(name: string) {
    return (await cookies()).get(name)?.value;
  },
  async set(name: string, value: string, options: any) {
    (await cookies()).set(name, value, options);
  },
  async delete(name: string) {
    (await cookies()).delete(name);
  },
};

// Password hashing
export const hash = createHash({ rounds: 12 });

// Session auth — user() fetches from database on every request
export const auth = createAuth({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  resolveUser: (id) => db.user.findUnique({ where: { id } }),
  resolveUserByCredentials: (creds) =>
    db.user.findUnique({ where: { email: creds.email } }),
  hash,
});

// Token verifier (password reset) — create one verifier per purpose,
// sharing the same secret
export const tokenVerifier = createTokenVerifier({
  secret: process.env.IDEAL_AUTH_SECRET!,
  purpose: 'password-reset',
  expiryMs: 1000 * 60 * 60, // 1 hour
});

// TOTP for 2FA
export const totp = createTOTP();

// Rate limiter for login
export const loginLimiter = createRateLimiter({
  maxAttempts: 5,
  windowMs: 15 * 60 * 1000, // 15 minutes
});
```
### Cookie-backed

```ts title="lib/auth.ts"
import {
  createAuth,
  createHash,
  createRateLimiter,
} from 'ideal-auth';
import { cookies } from 'next/headers';
import { db } from './db';

// TUser is the session user — no password
type User = {
  id: string;
  email: string;
  name: string;
  role: string;
};

const cookieBridge = {
  async get(name: string) {
    return (await cookies()).get(name)?.value;
  },
  async set(name: string, value: string, options: any) {
    (await cookies()).set(name, value, options);
  },
  async delete(name: string) {
    (await cookies()).delete(name);
  },
};

export const hash = createHash({ rounds: 12 });

// Session auth — user() reads from cookie, zero database calls after login
export const auth = createAuth<User>({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  sessionFields: ['email', 'name', 'role'],
  resolveUserByCredentials: (creds) =>
    db.user.findUnique({ where: { email: creds.email } }),
  hash,
});

export const loginLimiter = createRateLimiter({
  maxAttempts: 5,
  windowMs: 15 * 60 * 1000,
});
```
### Cookie-backed (no database)

```ts title="lib/auth.ts"
import { createAuth } from 'ideal-auth';

type User = {
  id: string;
  email: string;
  name: string;
  accessToken: string;
};

const cookieBridge = {
  async get(name: string) {
    return (await cookies()).get(name)?.value;
  },
  async set(name: string, value: string, options: any) {
    (await cookies()).set(name, value, options);
  },
  async delete(name: string) {
    (await cookies()).delete(name);
  },
};

// No database — user info comes from OIDC and is stored in the cookie
export const auth = createAuth<User>({
  secret: process.env.IDEAL_AUTH_SECRET!,
  cookie: cookieBridge,
  sessionFields: ['email', 'name', 'accessToken'],
  attemptUser: async (credentials) => {
    // Validate the cross-domain transfer token
    const data = await validateTransferToken(credentials.id, credentials.token);
    if (!data) return null;

    // Fetch user profile from identity provider
    const res = await fetch('https://identity.example.com/api/userinfo', {
      headers: { Authorization: `Bearer ${data.accessToken}` },
    });
    if (!res.ok) return null;
    const profile = await res.json();

    return {
      id: profile.sub,
      email: profile.email,
      name: profile.name,
      accessToken: data.accessToken,
    };
  },
});
```

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