Docs/Reference/Gateway

Gateway

The authentication gateway. Everything related to proving who the caller is: passwords, tokens, guests, passwordless codes, SSO.

All endpoints are under /v1/gateway. The refresh cookie (katforge_refresh) is HttpOnly, Secure, SameSite=none, scoped to /v1/gateway.

Login

POST /v1/gateway/login

Authenticate with email or username and password. Returns an access token in the body plus a refresh token in an HttpOnly cookie. The identifier auto-detects email vs username based on whether it contains @.

Wrong credentials return 401 regardless of whether the account exists, to prevent enumeration.

  • Auth: Public
  • Rate limit: 30 / 60s

Request

JSON
{
   "identifier": "anders@example.com",
   "password": "hunter2"
}
FieldTypeConstraints
identifierstringrequired, not blank. Email or username.
passwordstringrequired, not blank.

Response200 OK

JSON
{
   "access_token": "eyJhbGciOi…",
   "refresh_token": "eyJhbGciOi…",
   "token_type": "Bearer",
   "expires_in": 3600,
   "player": {
      "id": 42,
      "name": "anders",
      "is_guest": false,
      "roles": [ "ROLE_REGISTERED" ],
      "avatar_url": "https://api.katforge.com/v1/players/42/avatar?v=1713456000",
      "avatar": { "source": "custom", "icon": "lorc/dragon-head", "color": "cyan", "bg": "crimson" },
      "email": "anders@example.com",
      "username": "anders",
      "display_name": "Anders",
      "locale": "en",
      "timezone": "America/New_York",
      "last_login_at": "2026-04-17T22:04:11+00:00",
      "created_at": "2024-08-03T17:02:00+00:00",
      "email_verified_at": "2024-08-03T17:05:00+00:00",
      "channels": [ "email:product" ]
   }
}

Sets cookie katforge_refresh (TTL 30 days).

Errors

Codeflash.errors[].codeMeaning
401auth:invalidWrong identifier or password.
423auth:lockedAccount temporarily locked after repeated failures.
422validation:failedMissing or blank fields.
429rate_limit:exceeded30 per minute exceeded.

Passwordless codes

POST /v1/gateway/code/request

Request a 6-digit login code delivered by email (or SMS, if the account has a verified phone and channel=sms). Always returns 200 regardless of whether the email exists, to prevent enumeration. The code expires after 5 minutes.

  • Auth: Public
  • Rate limit: 5 / 60s

Request

JSON
{
   "email": "anders@example.com",
   "channel": "email"
}
FieldTypeConstraints
emailstringrequired.
channelstringoptional. email (default) or sms.

Response200 OK, empty body.

Errors

CodeMeaning
400email missing.
4295 per minute exceeded.

POST /v1/gateway/code/verify

Verify a passwordless code and mint tokens. The code is single-use and expires after 5 minutes.

  • Auth: Public
  • Rate limit: 10 / 60s

Request

JSON
{
   "email": "anders@example.com",
   "code": "482317"
}

Response200 OK

Same shape as POST /v1/gateway/login. Sets katforge_refresh cookie.

Errors

CodeMeaning
400email or code missing.
401Invalid or expired code.

Logout

POST /v1/gateway/logout

Clear the refresh cookie. Access tokens are stateless JWTs and cannot be invalidated server-side; clients should drop them on logout.

  • Auth: Public

Request — No body.

Response200 OK, empty body. Clears the katforge_refresh cookie.


Refresh

POST /v1/gateway/refresh

Exchange a refresh token for a fresh access token. Also rotates the refresh token. The refresh token is read from the katforge_refresh cookie (browser) or the request body (Capacitor / native clients). The cookie takes precedence.

For guest sessions, the response also includes a reclaim_token that lets the same player ID be recovered from a different device.

  • Auth: Public (but requires a valid refresh token)

Request

JSON
{
   "refresh_token": "eyJhbGciOi…"
}

refresh_token is optional if the cookie is present.

Response200 OK

JSON
{
   "access_token": "eyJhbGciOi…",
   "refresh_token": "eyJhbGciOi…",
   "token_type": "Bearer",
   "expires_in": 3600,
   "player": { "…": "as on /login" },
   "reclaim_token": "eyJhbGciOi…"
}

reclaim_token only appears for guest sessions. Sets rotated katforge_refresh.

Errors

CodeMeaning
401Refresh token missing, invalid, or expired.

Guest sessions

POST /v1/gateway/guest

Create a guest session. The returned access token lets the caller play games and appear on leaderboards without registering. Guests have a real Player.id.

Optionally accepts a reclaimToken from a previous guest session to recover the same player ID — useful when a guest installs the mobile app after playing on the web, or when their cookies are wiped between sessions but their reclaim token was stashed in app storage. If username is omitted, one is auto-generated (e.g. Brave_Lion_42).

  • Auth: Public
  • Rate limit: 60 / 60s

Request

JSON
{
   "username": "Brave_Lion_42",
   "reclaimToken": "eyJhbGciOi…"
}
FieldTypeConstraints
usernamestring|nulloptional. 3–20 chars, [A-Za-z0-9_]. Must be unique.
reclaimTokenstring|nulloptional. Signed JWT from a prior guest session.

Response200 OK

Same shape as POST /v1/gateway/login plus a reclaim_token field. Sets katforge_refresh with a 2-year TTL.

JSON
{
   "access_token": "eyJhbGciOi…",
   "refresh_token": "eyJhbGciOi…",
   "token_type": "Bearer",
   "expires_in": 3600,
   "player": {
      "id": 108,
      "name": "Brave_Lion_42",
      "is_guest": true,
      "roles": [ "ROLE_GUEST" ]
   },
   "reclaim_token": "eyJhbGciOi…"
}

Errors

CodeMeaning
409Username already taken.
500Unable to generate unique username (extreme contention).

POST /v1/gateway/upgrade

Upgrade a guest session to a registered account. Preserves the player ID, so leaderboard entries, characters, stats, and favorites all survive. Requires the caller to be authenticated as a guest.

  • Auth: Authenticated (guest)
  • Rate limit: 10 / 60s

Request

JSON
{
   "email": "anders@example.com",
   "password": "hunter22-longer",
   "display_name": "Anders"
}
FieldTypeConstraints
emailstringrequired, valid email, must be unique.
passwordstringrequired, min 8 chars.
display_namestring|nulloptional.

Response200 OK

Same shape as POST /v1/gateway/login with the upgraded user's roles. Sets katforge_refresh with the standard 30-day TTL.

Errors

CodeMeaning
409Email already registered.
422Validation failed.

Password reset

POST /v1/gateway/reset-password/request

Request a password reset email. Always returns 200 regardless of whether the email is registered.

  • Auth: Public
  • Rate limit: 5 / 60s

Request

JSON
{
   "email": "anders@example.com"
}

Response200 OK, empty body.

POST /v1/gateway/reset-password

Complete a password reset using the token from the email.

  • Auth: Public
  • Rate limit: 5 / 60s

Request

JSON
{
   "token": "eyJhbGciOi…",
   "password": "new-hunter22"
}
FieldTypeConstraints
tokenstringrequired. Single-use JWT from the reset email.
passwordstringrequired, min 8 chars.

Response200 OK, empty body.

Errors

Codeflash.errors[].codeMeaning
401auth:token_invalidToken invalid, expired, or already used.
422validation:failedPassword too short.

Cross-domain SSO

GET /v1/gateway/customs

Complete a cross-domain SSO handoff. Exchange a passport (minted via GET /v1/tokens/passport) for a full session on a different KATFORGE property. Validates the passport, mints fresh tokens, sets the refresh cookie on the destination domain, and 302s to redirect.

redirect must be on an allowed KATFORGE-owned domain.

  • Auth: Public (passport in query string)

Query params

FieldConstraints
passportrequired. Short-lived passport JWT (~60s TTL).
redirectrequired. Absolute URL on an allowed KATFORGE domain.

Response302 redirect to redirect. Sets katforge_refresh on the destination domain.

Errors

CodeMeaning
400redirect missing or not on an allowed domain.
401Passport missing, invalid, or expired.