OAuth
Two distinct surfaces:
- OAuth provider flows (
/v1/gateway/oauth) — consume upstream providers (Discord, Google, Apple, Steam,
KATFORGE) to sign users into
KATFORGE. - OAuth 2.0 server (
/v1/oauth) — act as an OAuth 2.0 authorization server so third-party apps can sign users in with
KATFORGE (PKCE, authorization-code grant).
Provider flows
Supported providers: discord, google, apple, steam, katforge.
GET /v1/gateway/oauth/{provider}/url
Get the authorization URL for a provider. Redirect the browser here to begin the OAuth dance. The redirect parameter must be on an allowed
KATFORGE domain and is preserved across the round-trip.
- Auth: Public
- Rate limit: 30 / 60s
Path params
| Field | Constraints |
|---|---|
provider | discord | google | apple | steam | katforge. |
Query params
| Field | Constraints |
|---|---|
redirect | required. Frontend URL on an allowed |
Response — 200 OK
{
"url": "https://discord.com/api/oauth2/authorize?client_id=…&state=…&redirect_uri=…"
}
Errors
| Code | Meaning |
|---|---|
400 | Unknown provider, missing redirect, or disallowed domain. |
GET /v1/gateway/oauth/{provider}/check
Handle the OAuth callback. Called by the provider (not by your app) after the user authorizes. Always responds with a 302 to the originating frontend URL, carrying one of three outcomes.
- Auth: Public
- Path params:
provider=discord|google|apple|steam.
Query params
| Field | Constraints |
|---|---|
state | required. Opaque state from the authorization URL. |
code | required for all providers except Steam. |
Response — 302 redirect with one of:
- Existing user:
?access_token=…&created=0(setskatforge_refresh). - New user needs username:
?needs_username=1&temp_token=…&suggested_username=…&email=…. - Error:
?error=<message>.
POST /v1/gateway/oauth/complete
Complete registration when /check returned needs_username=1. The user chooses a username; this endpoint creates the account, links the OAuth identity, and mints tokens.
- Auth: Public
- Rate limit: 10 / 60s
Request
{
"temp_token": "eyJhbGciOi…",
"username": "anders"
}
Response — 200 OK
Same shape as POST /v1/gateway/login. Sets katforge_refresh.
Errors
| Code | Meaning |
|---|---|
400 | temp_token or username missing, or temp_token invalid. |
POST /v1/gateway/oauth/{provider}/link
Link an OAuth provider to the currently-authenticated user. After linking, the user can sign in with either method.
- Auth:
ROLE_REGISTERED - Rate limit: 10 / 60s
Path params: provider = discord | google | apple | steam.
Query params
| Field | Constraints |
|---|---|
state | required. |
code | required (not Steam). |
Response — 200 OK, empty body.
DELETE /v1/gateway/oauth/{provider}
Unlink an OAuth provider. Refuses if it would leave the account with no remaining sign-in method.
- Auth:
ROLE_REGISTERED - Path params:
provider=discord|google|apple|steam|katforge.
Response — 200 OK, empty body.
GET /v1/gateway/oauth
List the OAuth providers linked to the current user.
- Auth:
ROLE_REGISTERED
Response — 200 OK
{
"providers": [ "discord", "google" ]
}
OAuth 2.0 server
First-party OAuth 2.0 authorization server. Only authorization_code grant, PKCE S256.
GET /v1/oauth/authorize/validate
Validate a client before rendering the consent UI. Lets the consent screen show app metadata (name, first-party flag, scopes) without exposing validation logic to the browser.
- Auth: Public
- Rate limit: 30 / 60s
Query params: client_id, redirect_uri.
Response — 200 OK
{
"client": {
"id": "app-abc123",
"name": "Stumper Companion",
"is_first_party": false,
"scopes": [ "profile", "email" ]
}
}
Errors
| Code | Meaning |
|---|---|
400 | Missing client_id or redirect_uri, or pair is invalid. |
POST /v1/oauth/authorize
Issue an authorization code after the user clicks "Authorize" in the consent UI. The code is one-shot and bound to the client, redirect URI, and PKCE challenge.
- Auth: Authenticated (guest or registered)
- Rate limit: 10 / 60s
Request (form or JSON)
| Field | Constraints |
|---|---|
client_id | required. |
redirect_uri | required. Must match a URI registered with the client. |
response_type | optional, default code. Only code is supported. |
state | optional. Echoed back to the caller. |
scope | optional, default profile email. Space-separated. |
code_challenge | optional. PKCE challenge. |
code_challenge_method | optional, default S256. Only S256 is supported. |
Response — 200 OK
{
"code": "auth_abc123…",
"redirect_uri": "https://app.example.com/oauth/callback",
"state": "xyz"
}
Errors
| Code | Meaning |
|---|---|
400 | response_type not code, missing fields, or unsupported PKCE method. |
POST /v1/oauth/token
Exchange an authorization code for an access token. Standard OAuth 2.0 token endpoint.
- Auth: Public (client authenticates via
client_secretorcode_verifier) - Rate limit: 20 / 60s
/v1/oauth/tokenRequest (form or JSON)
| Field | Constraints |
|---|---|
grant_type | required. Must be authorization_code. |
code | required. Code from /authorize. |
client_id | required. |
redirect_uri | required. Must match the URI used at /authorize. |
client_secret | required for confidential clients. |
code_verifier | required for public (PKCE) clients. |
Response — 200 OK
{
"access_token": "eyJhbGciOi…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "profile email"
}
Errors
| Code | Meaning |
|---|---|
400 | Unsupported grant, missing fields, or client auth absent. |
GET /v1/oauth/userinfo
Return the authenticated user's profile, filtered by granted scopes. Tokens without a scope claim (i.e. gateway tokens, not OAuth-server tokens) get the full profile.
- Auth: Bearer token
- Rate limit: 30 / 60s
Response — 200 OK
{
"sub": "42",
"email": "anders@example.com",
"name": "Anders",
"preferred_username": "anders",
"picture": "https://api.katforge.com/v1/players/42/avatar?v=1713456000"
}
Fields depend on granted scopes. sub is always present.