Authentication¶
Who is this page for?
Integrators choosing a credential, and anyone debugging a 401/403.
The enforcement internals (scoping, rate limits, secrets) live in
Security.
Two credentials, one decision¶
Almost every endpoint is guarded by the dependency
auth_api_key_or_oauth2. It accepts either:
Credential |
Header |
Best for |
|---|---|---|
API key |
|
Server-to-server integration. Simple, long-lived, scoped to the data the key created. |
OAuth2 bearer |
|
Interactive users/admins via Keycloak (OIDC). Required for privileged and per-user operations. |
The resolution order is API key first, OAuth2 second: if a valid
X-API-Key is present the request is authenticated immediately; otherwise
the bearer token is validated. If neither is valid the request fails with
401 Invalid authentication credentials.
Note
A handful of endpoints require OAuth2 specifically and reject API keys -
notably the per-user simulation endpoint
(GET /games/{gameId}/users/{externalUserId}/points/simulated), which is
bound to the token’s own subject. Those endpoints use the stricter
auth_oauth2 dependency. The endpoint’s OpenAPI entry always states
which it requires.
API keys¶
Issuing a key¶
Keys are minted by POST /api/v1/apikey/create, which is itself
OAuth2-protected - so you need a bearer token (and the admin role) to create
one:
curl -s -X POST "http://localhost:8000/api/v1/apikey/create" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"client":"my-service"}'
The response contains the apiKey string. Store it as a secret; it is the
bearer of all authority for that client.
Using a key¶
Send it on every request:
curl -s "http://localhost:8000/api/v1/games" -H "X-API-Key: $API_KEY"
Properties to know:
A key can be active or inactive. An inactive/unknown key yields
403 API key is invalid or does not exist.Validation results are cached briefly (
API_KEY_HEADER_CACHE_TTL_SECONDS, default 5 s) to avoid a DB hit per request. With the default in-memory cache each worker caches independently, so a revocation propagates on the next request after the TTL; setAPIKEY_CACHE_BACKEND=redisto share the cache (and revocations) across workers. See Configuration Reference.Every write made with a key stamps
apiKey_usedon the row, which both builds an audit trail and scopes what that key can later read (see Security).
Warning
The current API has no key revoke/delete endpoint. Treat key issuance as
deliberate, and prefer one key per integration/client so you can reason
about blast radius.
OAuth2 with Keycloak¶
GAME validates RS256 JWTs issued by Keycloak (OpenID Connect). The
validation (app/middlewares/valid_access_token.py) is strict:
The signing key is fetched from the realm JWKS endpoint (
/realms/<realm>/protocol/openid-connect/certs) via aPyJWKClientthat caches keys for 300 s.The token is decoded and checked for:
signature (RS256, against the JWKS key),
issuer =
<KEYCLOAK_URL>/realms/<KEYCLOAK_REALM>,audience =
KEYCLOAK_AUDIENCE(defaultaccount),expiry, with 30 s of clock-skew leeway.
The subject is taken from
sub, falling back throughpreferred_username,email,client_id,azp- so both user and service-account tokens resolve to a stable subject.
Failure modes map to precise responses:
Condition |
Status |
Detail |
|---|---|---|
Invalid signature |
|
|
Expired token |
|
|
Wrong audience |
|
|
Other malformed token |
|
|
JWKS fetch failure |
|
|
Getting a token (dev)¶
For local development you can use the resource-owner password grant:
TOKEN=$(curl -s -X POST \
"$KEYCLOAK_URL/realms/$KEYCLOAK_REALM/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=$KEYCLOAK_CLIENT_ID" \
-d "client_secret=$KEYCLOAK_CLIENT_SECRET" \
-d "grant_type=password" \
-d "username=game_admin" \
-d "password=$KEYCLOAK_USER_WITH_ROLE_PASSWORD" | jq -r '.access_token')
Swagger UI at /docs is also wired for the OAuth2 authorization-code flow
when KEYCLOAK_CLIENT_ID/KEYCLOAK_CLIENT_SECRET are configured, so you
can authorize interactively and try endpoints in the browser.
The admin role¶
A bearer token carrying the realm role ``AdministratorGAME`` is treated as an admin. Admins bypass the per-key/per-subject data scoping described in Security and can perform privileged operations (such as issuing API keys). Non-admin tokens are scoped to the games and users associated with their subject.
Identity bootstrapping¶
The first time a valid token is seen for a new subject, GAME creates an
OAuthUsers record (provider=keycloak, status=active) and writes a
single auth / OAuth user bootstrapped audit log entry. No manual user
provisioning is required - authenticating once is enough to register the
identity.
The per-request auth context¶
Internally, every guarded handler receives an AuthContext (and an
AuditLogger bound to it) carrying:
Field |
Meaning |
|---|---|
|
The validated API key string, if one was presented. |
|
The token subject ( |
|
Whether the token carries |
|
The decoded JWT claims. |
Handlers pass these into services as scoping parameters
(api_key, oauth_user_id, is_admin), which is how authorization is
enforced consistently across the codebase. See Security for the exact
rules.
Quick reference¶
You see… |
It means… |
|---|---|
|
No valid |
|
The |
|
Authenticated, but the credential is scoped away from that game. |
|
Authenticated, but a rate limit / daily quota was exceeded (Security). |