Docs/Guides/Authentication

Authentication

KATforge uses stateless JWT access tokens. Every authenticated endpoint expects an Authorization: Bearer <token> header. Tokens are issued by the gateway endpoints under /v1/gateway/.

Token types

KATforge issues five distinct token types, each with its own purpose and lifetime:

TypeIssued byLifetimePurpose
accessGateway login / OAuth / guest~1 hourBearer token for normal API calls
refreshGateway login / OAuth / guest~30 daysExchanged for fresh access tokens
passportGET /v1/tokens/passport~60 secondsCross-domain SSO redirects
password_resetPOST /v1/gateway/reset-password/request~1 hourSingle-use password reset
mercurePOST /v1/games/stumper/lobbies/{code}/token~24 hoursSubscribe to a single Stumper lobby's SSE topic

The refresh token is delivered as an HttpOnly cookie scoped to /v1/gateway/refresh, so browser clients never need to handle it directly. Capacitor and other non-browser clients pass it explicitly in the request body.

Email / password login

TypeScript
const session = await katforge.auth.login ('anders@example.com', 'hunter2');
//   or by username:
const session = await katforge.auth.login ('anders', 'hunter2');

The identifier field accepts either an email or a username — detection is automatic based on the presence of @. Wrong credentials always return 401 Invalid credentials regardless of which field exists, so attackers can't enumerate accounts.

Rate limited to 10 requests per minute per IP.

Guest sessions

Guests are first-class players. They have a real Player.id, can submit results, and appear on leaderboards. The only thing they can't do is log back in from a different device — unless you save the reclaim_token returned alongside the access token.

TypeScript
const session = await katforge.auth.guest ();

// Save this somewhere durable so the user can recover the same player ID later
localStorage.setItem ('katforge:reclaim', session.reclaim_token);

// Later, on a different device or after reinstall
const restored = await katforge.auth.guest ({
   reclaimToken: localStorage.getItem ('katforge:reclaim')
});

Guests can be upgraded to full accounts later, preserving their player ID and all game data:

TypeScript
await katforge.auth.upgrade ({
   email:    'anders@example.com',
   password: 'hunter2'
});

Refreshing tokens

Access tokens expire after about an hour. The SDK refreshes them automatically on 401 responses — you should never have to think about this. If you're calling the API directly:

shell
# Browser: cookie sent automatically
curl -X POST https://api.katforge.com/v1/gateway/refresh \
   --cookie 'kat_refresh=...'

# Capacitor / non-browser: pass it in the body
curl -X POST https://api.katforge.com/v1/gateway/refresh \
   -H 'Content-Type: application/json' \
   -d '{ "refresh_token": "..." }'

OAuth (Discord, Google, Apple, Steam)

See the OAuth Providers guide for the full social-login flow.

Cross-domain SSO

When the user is logged in on katforge.com and clicks through to lextris.com, you don't want them to log in again. KATforge solves this with passport tokens:

  1. Originating site calls GET /v1/tokens/passport with its access token to mint a short-lived passport.
  2. It redirects the browser to https://api.katforge.com/v1/gateway/customs?passport=<jwt>&redirect=https://lextris.com/play.
  3. The customs handler validates the passport, mints a fresh access/refresh pair, sets the refresh cookie on the destination domain, and redirects the browser to redirect.
TypeScript
// On katforge.com
const { passport, expires_in } = await katforge.auth.passport ();
window.location.href = `https://api.katforge.com/v1/gateway/customs`
   + `?passport=${encodeURIComponent (passport)}`
   + `&redirect=${encodeURIComponent ('https://lextris.com/play')}`;

The passport JWT is intentionally short-lived (~60 seconds) — long enough to survive a browser redirect, short enough that a leaked URL is useless. The destination must be on a KATforge-owned domain.

Logout

TypeScript
await katforge.auth.logout ();

Stateless JWTs cannot be invalidated server-side. Logout simply clears the refresh cookie and tells the SDK to drop its in-memory access token. The current access token will continue to work until it expires (≤1 hour). For high-stakes scenarios, rotate the user's password.

Errors

StatusMeaning
401 UnauthorizedMissing, invalid, or expired token
403 ForbiddenToken is valid but the action requires more privilege
429 Too Many RequestsRate limit hit; check the Retry-After header

See Errors for the full error format.