User authorisation: authorization code with PKCE

A player allows your app to act for them, with named scopes, on a consent screen on africaone.io. Your app never sees the player’s session, e-mail, player ID, balances or location: it gets a single-use code, a 300-second access token whose sub is a per-app pairwise ID, and a rotating refresh token.

What the player sees

The consent screen shows your app’s name, each scope in plain words, the ID your app will see for them, the host you registered for the return trip, and Allow / Not now. Nothing is shared until they allow. The receipt records the scopes and the consent-text version. They can withdraw it at any time; from then on every access token issued under it answers 403 CONSENT_REVOKED on its next use and every refresh token is dead.

1. Register a redirect URI

In the sandbox console, add the exact URL the browser should return to: https:// with a host, or http://127.0.0.1, http://[::1] or http://localhost (any port) for local development. No fragment, no credentials, no wildcard. Matching is exact. At most five per app.

2. Send the player to the consent screen

Generate a PKCE verifier (43 to 128 unreserved characters) and its S256 challenge, keep the verifier server-side with the state, then redirect the browser to /oauth/authorize on the player client with response_type=code, client_id, redirect_uri, scope, state, code_challenge and code_challenge_method=S256. Only S256 is accepted. The endpoints and the scope list are published at GET /developer/v1/.well-known/oauth-authorization-server.

Outcome Query on your registered redirect URI
Player allowed code=aoa_…&state=…&iss=… (120 seconds, once)
Player declined or the screen expired error=access_denied
A scope your app is not granted error=invalid_scope
Wrong response type, or no PKCE error=unsupported_response_type / error=invalid_request

An unknown client ID or an unregistered redirect URI never redirects: the player sees a refusal on africaone.io and nothing is shared.

3. Redeem the code from your server

POST /developer/v1/oauth/token with HTTP Basic client authentication, grant_type=authorization_code, code, the exact redirect_uri and code_verifier. The answer carries access_token (300 seconds), scope and refresh_token. A code is redeemable exactly once: a second redemption fails with invalid_grant and revokes the consent and every token issued under it. Treat it as a theft signal and send the player through the consent screen again.

4. Use the token

GET /developer/v1/me returns sub (stable for this player in this app, different in every other app) and display_name with profile.basic.read. GET /developer/v1/cities works with a user or an app token. An app token cannot call /me (403 USER_TOKEN_REQUIRED) and never carries a user scope.

5. Refresh and revoke

grant_type=refresh_token rotates the refresh token: the old one stops working; keep the new one. Presenting a refresh token that was already rotated revokes the whole family and the consent. A refresh may narrow the scopes, never widen them. POST /developer/v1/oauth/revoke with token= the refresh token ends the consent immediately (RFC 7009). Refresh tokens last 30 days.

Scopes that exist

Scope Audience Grants
city.public.read app or user GET /cities: the reviewed city registry projection
profile.basic.read user only GET /me: the pairwise sub and the chosen display name

Every other scope in the platform direction does not exist yet and is not grantable; commerce, entitlements, game sessions and regulated assets stay disabled and fail closed.

Why the session cookie never meets your app

The consent decision is a normal first-party call on africaone.io, with the same protections as every other change a player makes there. The only thing that crosses to your app is the return URL the server built from your registered redirect URI. The token endpoint takes no idempotency header because standard OAuth clients cannot send one; the single-use code and the rotating refresh token are the replay control.