Skip to content

HTTP-only cookies

Cookies are convenient for browser applications because JavaScript cannot read HTTP-only token cookies. The browser sends them automatically, so every state-changing cookie request also needs CSRF protection.

Configure cookies

ts
import { createAuth } from "@0-auth/zero-auth";

const auth = createAuth({
  accessSecret: process.env.JWT_ACCESS_SECRET!,
  refreshSecret: process.env.JWT_REFRESH_SECRET!,
  cookies: {
    options: {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
    },
  },
});

Use secure: true over HTTPS in production. Match the cookie path and domain to the routes that need the cookies.

Login, protect, refresh, and logout

ts
app.post("/auth/login", async (req, res, next) => {
  try {
    const user = await users.verifyCredentials(req.body.email, req.body.password);
    if (!user) {
      res.status(401).json({ error: "Invalid credentials" });
      return;
    }

    await auth.sendAuthTokens(res, {
      id: user.id,
      email: user.email,
      role: user.role,
      permissions: user.permissions,
    });
    res.json({ user: { id: user.id, email: user.email } });
  } catch (error) {
    next(error);
  }
});

app.get("/api/me", auth.protect(), (req, res) => {
  res.json({ user: req.user });
});

app.post("/auth/refresh", auth.refreshHandler());

app.post("/auth/logout", (_req, res) => {
  auth.clearAuth(res);
  res.json({ message: "Logged out" });
});

sendAuthTokens() sets HTTP-only access and refresh cookies. clearAuth() clears them using the configured cookie attributes.

CSRF protection

Mount the middleware before state-changing routes:

ts
app.use(auth.csrf());

app.get("/auth/csrf-token", (_req, res) => {
  res.json({ csrfToken: auth.csrfToken(res) });
});

The browser first obtains the token, stores the readable csrf_token cookie, and copies the returned value into the configured x-csrf-token header:

ts
const csrfResponse = await fetch("/auth/csrf-token", {
  credentials: "include",
});
const { csrfToken } = await csrfResponse.json();

await fetch("/api/account", {
  method: "POST",
  credentials: "include",
  headers: { "x-csrf-token": csrfToken },
});

By default, POST, PUT, PATCH, and DELETE are checked when an access or refresh auth cookie is present. GET, HEAD, and unauthenticated requests pass through. A missing, mismatched, tampered, or malformed token returns 403 AUTH_CSRF_INVALID.

Customize the cookie name, header name, or methods with CsrfConfig.

Cross-origin browsers

For a frontend on another origin, configure an allow-listed CORS policy and send credentials. Do not combine credentials with Access-Control-Allow-Origin: *. Use sameSite: "none" only when the deployment truly requires a cross-site cookie and always pair it with HTTPS and secure: true.

See the security checklist before deploying cookie sessions.