Skip to content

Auth System

OAuth Login (Desktop PKCE)

User clicks "Login with Google"
  → AuthService: generate code_verifier + code_challenge (SHA256)
  → Start local HTTP listener on random port
  → Open browser: /v1/auth/login/google?redirect_uri=localhost:{port}&code_challenge=...
  → API: set apex_redirect cookie, redirect to Google
  → User authorizes → Google redirects to /v1/auth/callback/google
  → API: authenticate, find/create UserEntity, issue one-time code
  → Browser redirects to localhost:{port}/callback?code=...
  → AuthService: POST /v1/auth/exchange → JWT + refresh token
  → Store tokens in auth.dat → LoginCompleted event
  → ShellViewModel shows authenticated UI

Auth & JWT Tokens

Access Token (JWT)

Issued by: JwtService.IssueAccessToken(user)

TTL: 60 minutes (configurable via Jwt__ExpiryMinutes)

Claims:

Claim Value Why it's included
sub User UUID Primary identity; used to look up user in all authorized endpoints
email User email Profile display without extra DB query
display_name User display name Leaderboard entries use this without joining users table
avatar_url Avatar URL Feed entries use this without joining
jti New UUID per token JWT ID for future token revocation lists
iss "apexlab-api" Validated against Jwt__Issuer config; must match exactly
aud "apexlab-client" Audience validation
exp Unix timestamp Token expiry

Storage on client: Encrypted in %LocalAppData%\ApexLab\auth.dat via DPAPI.

Usage: Sent as Authorization: Bearer {token} header on every authenticated API request. The client checks ExpiresAt before each call and calls POST /v1/auth/refresh if the token is expired or close to expiry.

Refresh Token

Format: Cryptographically secure random 32-byte value (256-bit entropy), returned as base64url string.

Storage on server: SHA-256 hash stored in refresh_tokens.token_hash — never the raw value.

Storage on client: Raw value stored encrypted in auth.dat.

Rotation: Every use produces a new token; the old one is revoked. This means a stolen refresh token can only be used once before the legitimate client detects the revocation on next refresh.

PKCE Code (one-time exchange code)

What it is: A short-lived code generated by the server after OAuth callback, passed back to the desktop app via the local redirect URI.

Why it's needed: The desktop app cannot receive an OAuth callback directly (it's not a web server). Instead, it starts a temporary HTTP listener on a random local port, passes that as the redirect URI, and waits for the code to arrive.

TTL: ~5 minutes (held in IAuthCodeStore, an in-memory dictionary).

How it's used: AuthService.ExchangeCodeAsync(code) sends the code to POST /v1/auth/exchange, which looks it up in IAuthCodeStore, returns the JWT and refresh token, and deletes the code entry.