go-auth
GuidesClient

Setup

The fetch wrapper every client-side example in these guides is built on: cookies, CSRF, and error handling.

Client

Every Client section across the guides — and every named method on the wrapped clients in this folder (authApi, and whatever gets added alongside future guides) — is built on one small fetch wrapper. It's defined once here rather than repeated in every guide.

It has to do two things a plain fetch call doesn't: send cookies on every request, and attach the CSRF header on every state-changing request. Both are mandatory — the double-submit token layer is on by default, so a POST without the header is a 403 before it reaches a handler.

function getCookie(name: string): string | null {
  const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
  return match ? decodeURIComponent(match[1]) : null;
}

async function getCSRFToken(baseUrl: string): Promise<string | null> {
  const cookieToken = getCookie("_csrf");
  if (cookieToken) return cookieToken;

  const res = await fetch(`${baseUrl}/auth/csrf-token`, {
    method: "GET",
    credentials: "include",
  });
  if (!res.ok) throw new Error("Could not initialize CSRF protection");

  // Same-origin and sibling-subdomain deployments receive 204 and can read
  // the cookie. Different registrable domains receive { token } instead.
  if (res.status === 204) return getCookie("_csrf");
  const payload: { token?: unknown } = await res.json();
  return typeof payload.token === "string" ? payload.token : null;
}

let onUnauthorized: (() => void) | null = null;

// Called from the Provider guide: lets the rest of the app react the moment
// any request comes back 401 or banned, instead of only the one call that hit it.
export function setUnauthorizedHandler(handler: () => void) {
  onUnauthorized = handler;
}

export async function apiRequest(baseUrl: string, method: string, path: string, body?: unknown) {
  const headers: Record<string, string> = {};
  if (body !== undefined) headers["Content-Type"] = "application/json";

  if (method !== "GET") {
    const csrf = await getCSRFToken(baseUrl);
    if (csrf) headers["X-CSRF-Token"] = csrf;
  }

  const res = await fetch(`${baseUrl}${path}`, {
    method,
    credentials: "include",                  // required — this is how the session cookie gets sent and stored
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  if (!res.ok) {
    const err = await res.json().catch(() => ({ error: res.statusText }));
    if (res.status === 401 || (res.status === 403 && err.error === "user_banned")) {
      onUnauthorized?.();
    }
    if (res.status === 429) {
      err.retryAfter = Number(res.headers.get("Retry-After")) || undefined;
    }
    throw err;
  }

  return res.status === 204 ? undefined : res.json();
}

credentials: "include" makes the browser send and store the HttpOnly session and refresh cookies. _csrf and X-CSRF-Token are the defaults; both are configurable on CSRFTokenConfig (see Configuration → WithSecurity). getCSRFToken reads the cookie for same-origin and sibling-subdomain deployments, and reads the { "token": "..." } body for different registrable domains. In the latter topology it calls /auth/csrf-token before each mutation, so it also picks up rotations that JavaScript cannot observe through document.cookie.

Leaving CSRFToken nil server-side does not disable the layer; the server creates the default configuration. With DisableCSRFToken: true, getCSRFToken returns null and the wrapper sends no CSRF header.

Use setUnauthorizedHandler to clear stale client state when any request finds that a session was revoked, banned, or expired. The Provider wires it to clear the app-wide user state on 401 or 403 user_banned.

On a 429, the thrown error also carries retryAfter: the Retry-After header value in seconds.

What lives where

  • The individual feature guides (e.g. Authentication) show the raw call — apiRequest(API_BASE, "POST", "/auth/login", ...) — right next to the curl and Go examples for that same endpoint, so you can see the request/response map 1:1 to what's documented above it.
  • Authentication, Sessions, Organizations, Security, OAuth linking, and Admin wrap those same calls into named methods — authApi.login(...), sessionsApi.list(...), orgApi.create(...), securityApi.changePassword(...), oauthApi.link(...), adminApi.listUsers(...) — the form you'd actually want in an app.
  • Provider wraps those in a React context, so components read user/loading from one hook instead of calling the API directly.
  • Middleware is a different layer entirely — it runs on the server, before any of the above, to redirect unauthenticated requests away from protected pages.

Next

On this page