Authentication & Sessions

Google OAuth 2.0 PKCE redirection, session cookie signatures, and transparent token rotations.

Arbiter implements a secure, stateful session cookie pattern built on top of the standard Google OAuth 2.0 protocol. Instead of static passwords, we utilize a stateless Proof Key for Code Exchange (PKCE) flow to exchange secure authorization grants.

The Login & Refresh Lifecycle

Client AppArbiter APIGoogle OAuthPostgres1. GET /api/auth/google2. Redirect (State & PKCE Challenge)3. Callback with auth code4. GET /callback?code=...5. Save User & Hash Refresh Token6. Set Cookies (JWT & Refresh)7. POST /api/auth/refresh (on 401)8. Rotated Cookies (JWT & New Refresh)
↔ Swipe horizontally to view full sequence flow

PKCE Mechanics

PKCE (Proof Key for Code Exchange) is essential to prevent authorization code hijacking. When starting a login request, the server generates a high-entropy code_verifier (using crypto.getRandomValues) and computes its SHA-256 hash, generating the code_challenge.

The verifier and state variables are encrypted into short-lived HTTP-only cookies (oauth_state and oauth_code_verifier) before redirecting to Google. On the callback redirect, the server verifies the state parameter (preventing CSRF) and exchanges the temporary auth code alongside the verifier cookie to obtain the profile tokens.

Transparent Retry via authFetch

Rather than forcing a hard redirect when a user session expires, the client wrapping utility handles 401 response statuses transparently. Look at the code in lib/client/authFetch.ts:

typescript
export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
  let res = await fetch(url, options);

  if (res.status === 401) {
    try {
      const refreshRes = await fetch('/api/auth/refresh', { method: 'POST' });
      if (refreshRes.ok) {
        // Refresh succeeded, retry the original request once
        res = await fetch(url, options);
      } else {
        // Refresh failed, dispatch event to trigger client logout
        if (typeof window !== 'undefined') {
          window.dispatchEvent(new Event('auth-failed'));
        }
      }
    } catch (err) {
      if (typeof window !== 'undefined') {
        window.dispatchEvent(new Event('auth-failed'));
      }
    }
  }

  return res;
}

If the access token expires, the client fetches the refresh route, which rotates the refresh token row in Postgres (invalidating the old hash and committing a new SHA-256 hash). If the refresh token is revoked or expired, the client is forced to logout.