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
{
"identifier": "anders@example.com",
"password": "hunter2"
}
| Field | Type | Constraints |
|---|---|---|
identifier | string | required, not blank. Email or username. |
password | string | required, not blank. |
Response — 200 OK
{
"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
| Code | flash.errors[].code | Meaning |
|---|---|---|
401 | auth:invalid | Wrong identifier or password. |
423 | auth:locked | Account temporarily locked after repeated failures. |
422 | validation:failed | Missing or blank fields. |
429 | rate_limit:exceeded | 30 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
{
"email": "anders@example.com",
"channel": "email"
}
| Field | Type | Constraints |
|---|---|---|
email | string | required. |
channel | string | optional. email (default) or sms. |
Response — 200 OK, empty body.
Errors
| Code | Meaning |
|---|---|
400 | email missing. |
429 | 5 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
{
"email": "anders@example.com",
"code": "482317"
}
Response — 200 OK
Same shape as POST /v1/gateway/login. Sets katforge_refresh cookie.
Errors
| Code | Meaning |
|---|---|
400 | email or code missing. |
401 | Invalid 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.
Response — 200 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
{
"refresh_token": "eyJhbGciOi…"
}
refresh_token is optional if the cookie is present.
Response — 200 OK
{
"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
| Code | Meaning |
|---|---|
401 | Refresh 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
{
"username": "Brave_Lion_42",
"reclaimToken": "eyJhbGciOi…"
}
| Field | Type | Constraints |
|---|---|---|
username | string|null | optional. 3–20 chars, [A-Za-z0-9_]. Must be unique. |
reclaimToken | string|null | optional. Signed JWT from a prior guest session. |
Response — 200 OK
Same shape as POST /v1/gateway/login plus a reclaim_token field. Sets katforge_refresh with a 2-year TTL.
{
"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
| Code | Meaning |
|---|---|
409 | Username already taken. |
500 | Unable 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
{
"email": "anders@example.com",
"password": "hunter22-longer",
"display_name": "Anders"
}
| Field | Type | Constraints |
|---|---|---|
email | string | required, valid email, must be unique. |
password | string | required, min 8 chars. |
display_name | string|null | optional. |
Response — 200 OK
Same shape as POST /v1/gateway/login with the upgraded user's roles. Sets katforge_refresh with the standard 30-day TTL.
Errors
| Code | Meaning |
|---|---|
409 | Email already registered. |
422 | Validation 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
{
"email": "anders@example.com"
}
Response — 200 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
{
"token": "eyJhbGciOi…",
"password": "new-hunter22"
}
| Field | Type | Constraints |
|---|---|---|
token | string | required. Single-use JWT from the reset email. |
password | string | required, min 8 chars. |
Response — 200 OK, empty body.
Errors
| Code | flash.errors[].code | Meaning |
|---|---|---|
401 | auth:token_invalid | Token invalid, expired, or already used. |
422 | validation:failed | Password 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
| Field | Constraints |
|---|---|
passport | required. Short-lived passport JWT (~60s TTL). |
redirect | required. Absolute URL on an allowed |
Response — 302 redirect to redirect. Sets katforge_refresh on the destination domain.
Errors
| Code | Meaning |
|---|---|
400 | redirect missing or not on an allowed domain. |
401 | Passport missing, invalid, or expired. |