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

FieldConstraints
providerdiscord | google | apple | steam | katforge.

Query params

FieldConstraints
redirectrequired. Frontend URL on an allowed KATFORGE domain.

Response200 OK

JSON
{
   "url": "https://discord.com/api/oauth2/authorize?client_id=…&state=…&redirect_uri=…"
}

Errors

CodeMeaning
400Unknown 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

FieldConstraints
staterequired. Opaque state from the authorization URL.
coderequired for all providers except Steam.

Response302 redirect with one of:

  • Existing user: ?access_token=…&created=0 (sets katforge_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

JSON
{
   "temp_token": "eyJhbGciOi…",
   "username": "anders"
}

Response200 OK

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

Errors

CodeMeaning
400temp_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

FieldConstraints
staterequired.
coderequired (not Steam).

Response200 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.

Response200 OK, empty body.

GET /v1/gateway/oauth

List the OAuth providers linked to the current user.

  • Auth: ROLE_REGISTERED

Response200 OK

JSON
{
   "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.

Response200 OK

JSON
{
   "client": {
      "id": "app-abc123",
      "name": "Stumper Companion",
      "is_first_party": false,
      "scopes": [ "profile", "email" ]
   }
}

Errors

CodeMeaning
400Missing 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)

FieldConstraints
client_idrequired.
redirect_urirequired. Must match a URI registered with the client.
response_typeoptional, default code. Only code is supported.
stateoptional. Echoed back to the caller.
scopeoptional, default profile email. Space-separated.
code_challengeoptional. PKCE challenge.
code_challenge_methodoptional, default S256. Only S256 is supported.

Response200 OK

JSON
{
   "code": "auth_abc123…",
   "redirect_uri": "https://app.example.com/oauth/callback",
   "state": "xyz"
}

Errors

CodeMeaning
400response_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_secret or code_verifier)
  • Rate limit: 20 / 60s
POST/v1/oauth/token

Request (form or JSON)

FieldConstraints
grant_typerequired. Must be authorization_code.
coderequired. Code from /authorize.
client_idrequired.
redirect_urirequired. Must match the URI used at /authorize.
client_secretrequired for confidential clients.
code_verifierrequired for public (PKCE) clients.

Response200 OK

JSON
{
   "access_token": "eyJhbGciOi…",
   "token_type": "Bearer",
   "expires_in": 3600,
   "scope": "profile email"
}

Errors

CodeMeaning
400Unsupported 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

Response200 OK

JSON
{
   "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.