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.