---
title: CSRF Protection
description: Generate and verify CSRF tokens with Bun's built-in API
---

`Bun.CSRF` generates and verifies [CSRF (Cross-Site Request Forgery)](https://owasp.org/www-community/attacks/csrf) tokens. Tokens are signed with HMAC and include an expiration timestamp.

```ts title="csrf.ts" icon="/icons/typescript.svg"
// Generate a token bound to the requester's session
const token = Bun.CSRF.generate("my-secret", { sessionId: "user-session-id" });

// Verify it
const isValid = Bun.CSRF.verify(token, { secret: "my-secret", sessionId: "user-session-id" });
console.log(isValid); // true
```

<Callout type="warning">
  Always pass a `sessionId` (the requester's session identifier or user ID) to both `generate()` and `verify()`. Without
  it, a token is only bound to the secret — any token the server has ever issued validates for every user, so an
  attacker can obtain a token in their own session and replay it in a forged cross-site request from a victim's browser.
</Callout>

---

## `Bun.CSRF.generate()`

Generate a CSRF token. The token contains a cryptographic nonce, a timestamp, and an HMAC signature, encoded as a string.

```ts title="generate.ts" icon="/icons/typescript.svg"
const token = Bun.CSRF.generate("my-secret-key");
```

**Parameters:**

- `secret` (string, optional) — The secret key used to sign the token. If not provided, Bun generates a random in-memory default secret (unique per thread).
- `options` (object, optional):

| Option      | Type     | Default       | Description                                                                                                                                                  |
| ----------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `expiresIn` | `number` | `86400000`    | Milliseconds until the token expires. Defaults to 24 hours.                                                                                                  |
| `encoding`  | `string` | `"base64url"` | Token encoding format: `"base64"`, `"base64url"`, or `"hex"`.                                                                                                |
| `algorithm` | `string` | `"sha256"`    | HMAC algorithm: `"sha256"`, `"sha384"`, `"sha512"`, `"sha512-256"`, `"blake2b256"`, or `"blake2b512"`.                                                       |
| `sessionId` | `string` | (none)        | Binds the token to the requesting principal (session ID, user ID, or equivalent). The token only verifies when the same `sessionId` is passed to `verify()`. |

**Returns:** `string` — the encoded token.

```ts title="generate-options.ts" icon="/icons/typescript.svg"
// Token bound to the requester's session that expires in 1 hour, encoded as hex
const token = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  expiresIn: 60 * 60 * 1000,
  encoding: "hex",
});

// Using a different algorithm
const token2 = Bun.CSRF.generate("my-secret", {
  sessionId: "user-session-id",
  algorithm: "sha512",
});
```

---

## `Bun.CSRF.verify()`

Verify a CSRF token. Returns `true` if the token is valid and has not expired, `false` otherwise.

```ts title="verify.ts" icon="/icons/typescript.svg"
const isValid = Bun.CSRF.verify(token, { secret: "my-secret-key" });
```

**Parameters:**

- `token` (string, required) — The token to verify.
- `options` (object, optional):

| Option      | Type     | Default       | Description                                                                                                                                                                                                          |
| ----------- | -------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `secret`    | `string` | (auto)        | The secret used to sign the token. If not provided, uses the same in-memory default as `generate()`.                                                                                                                 |
| `maxAge`    | `number` | `86400000`    | Maximum token age in milliseconds, independent of the token's own `expiresIn`.                                                                                                                                       |
| `encoding`  | `string` | `"base64url"` | Must match the encoding used during `generate()`.                                                                                                                                                                    |
| `algorithm` | `string` | `"sha256"`    | Must match the algorithm used during `generate()`.                                                                                                                                                                   |
| `sessionId` | `string` | (none)        | Must match the `sessionId` used during `generate()`. A token bound to one principal fails verification for any other principal, and a token generated without a `sessionId` fails verification when one is supplied. |

**Returns:** `boolean`

```ts title="verify-options.ts" icon="/icons/typescript.svg"
// Verify a token bound to the requester's session
const isValid = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
});

// Enforce a shorter max age than what the token was generated with
const isValid2 = Bun.CSRF.verify(token, {
  secret: "my-secret",
  sessionId: "user-session-id",
  maxAge: 60 * 1000, // reject tokens older than 1 minute
});
```

---

## Using with `Bun.serve()`

A typical pattern is to generate a token when rendering a form, embed it in a hidden field, and verify it when the form is submitted. Pass the requester's session identifier as `sessionId` to both calls so the token only works for the user it was issued to.

```ts title="server.ts" icon="/icons/typescript.svg"
const SECRET = process.env.CSRF_SECRET || "my-secret";

// Resolve the requester's session identifier from a session cookie. Returns
// null when the visitor has no session yet — never fall back to a shared
// placeholder, or every session-less visitor would share one token binding.
function getSessionId(req: Request): string | null {
  return req.headers.get("cookie")?.match(/(?:^|;\s*)session=([^;]+)/)?.[1] ?? null;
}

const server = Bun.serve({
  routes: {
    "/form": req => {
      // Create a per-visitor session before issuing the form so the token is
      // bound to this visitor and no one else.
      let sessionId = getSessionId(req);
      const headers = new Headers({ "Content-Type": "text/html" });
      if (!sessionId) {
        sessionId = crypto.randomUUID();
        headers.append("Set-Cookie", `session=${sessionId}; HttpOnly; SameSite=Lax; Path=/`);
      }

      const token = Bun.CSRF.generate(SECRET, { sessionId });

      return new Response(
        `<form method="POST" action="/submit">
          <input type="hidden" name="_csrf" value="${token}" />
          <input type="text" name="message" />
          <button type="submit">Send</button>
        </form>`,
        { headers },
      );
    },

    "/submit": {
      POST: async req => {
        const sessionId = getSessionId(req);
        const formData = await req.formData();
        const csrfToken = formData.get("_csrf");

        if (!sessionId || typeof csrfToken !== "string" || !Bun.CSRF.verify(csrfToken, { secret: SECRET, sessionId })) {
          return new Response("Invalid CSRF token", { status: 403 });
        }

        return new Response("OK");
      },
    },
  },
});

console.log(`Listening on ${server.url}`);
```

---

## Default secret

If you omit the `secret` parameter in both `generate()` and `verify()`, Bun uses a random secret generated once per thread. This is convenient for single-threaded applications, but tokens won't verify across servers or workers, or after a restart.

```ts title="default-secret.ts" icon="/icons/typescript.svg"
// Both calls use the same per-thread default secret within this runtime context.
const token = Bun.CSRF.generate();
const isValid = Bun.CSRF.verify(token); // true
```

For production use, always provide an explicit secret shared across your infrastructure.

---

## TypeScript

```ts title="types.ts" icon="/icons/typescript.svg"
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";

interface CSRFGenerateOptions {
  expiresIn?: number;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  sessionId?: string;
}

interface CSRFVerifyOptions {
  secret?: string;
  encoding?: "base64" | "base64url" | "hex";
  algorithm?: CSRFAlgorithm;
  maxAge?: number;
  sessionId?: string;
}

namespace Bun.CSRF {
  function generate(secret?: string, options?: CSRFGenerateOptions): string;
  function verify(token: string, options?: CSRFVerifyOptions): boolean;
}
```
