Authentication overview
This page summarizes how a player signs in to Open Play: which methods exist, which rules they all share, and which page to go to for details. If you are wiring up player sign-in, start here.
Sign-in methods
| Method | When? | SDK | Details |
|---|---|---|---|
| Guest | Quick start without an account; progress is kept on the same device | TS: signInGuest · Unity: SignInGuestAsync | Guest sign-in |
| Email and password | A permanent account; includes password reset and change | TS and Unity: sign-up, sign-in, upgrade, password operations | Email and password |
| Google (OAuth) | One-click web sign-in; if there is a verified email, it links automatically to the existing account | TS: startGoogleOAuth, completeGoogleOAuth · Unity: none | Sign in with Google |
| Native platform proof (Apple, Game Center, Play Games) | Native mobile sign-ins | No SDK helper yet; HTTP endpoint only | Sign in with Google › Native proofs |
| Magic link | A single-use link sent by email | No SDK helper yet; the server endpoint exists | Email and password › Magic link |
Which methods to offer players is the game’s choice; for Google to work, it must be configured in the environment (Game settings › Sign-in methods).
Topics after sign-in: Sessions and CSRF, Account linking, Server-side verification.
Which method should I choose?
- If an account is not required, start with guest; as the player progresses, link them to email or Google with guest upgrade. The player ID (
playerId) does not change. - If you want multiple devices, you need email/password or Google; a guest identity depends on the secret the device stores.
- For native mobile sign-in (Apple, Game Center, Play Games), HTTP endpoints exist, but the SDKs have no helper.
Rules that apply to every call
The rules below apply to all endpoints under /v1/auth/*.
1. Environment header. Every request carries x-crx-env: <environment-id>. If it is missing or not a UUID, the response is 401.
2. Content type. Requests with a body are sent with Content-Type: application/json.
3. CSRF. Every request other than GET requires double-submit CSRF protection: the __Secure-crx.csrf cookie and a matching x-csrf-token header. On every successful use the token is returned and changes. You first get a token with GET /v1/auth/csrf. Details: Sessions and CSRF.
4. Origin check. For state-changing requests coming from a browser, the Origin domain must be bound to the environment. See Architecture and hosting.
5. Field names are snake_case. Request and response bodies use snake_case (display_name, bootstrap_token, old_password). Two exceptions: displayName in the GET /v1/auth/session response, and the body/response of POST /v1/auth/session/display-name (displayName). The SDKs hide this difference from you and present camelCase.
6. Error bodies. Errors are mostly plain text (permission denied, rate limited…). The only meaningful error with a JSON body is the 403 {"code":"account_banned","message":"account banned"} response for a banned account. Status codes: 400 invalid input, 401 environment or session missing/invalid, 403 denied (wrong password, CSRF, expired token — the distinction is deliberately not leaked), 409 conflict, 422 unprocessable, 429 rate limit. Details: Errors.
Successful sign-in response
The sign-in, sign-up, refresh, and OAuth callback endpoints return the same envelope and write two cookies (__Secure-crx.session, __Secure-crx.refresh; HttpOnly; Secure; SameSite=Lax; Path=/, Max-Age=15552000 = 180-day upper bound):
{
"player": {
"id": "0192…",
"display_name": "Guest",
"email": null,
"email_verified": false,
"is_guest": true,
"image": null,
"created_at": "2026-11-03T10:00:00+00:00"
},
"tokens": {
"session_token": "<64 hex>",
"refresh_token": "<64 hex>",
"access_token": "<jwt>",
"expires_in_secs": 900
},
"is_new_player": true
}
| Field | Meaning |
|---|---|
player.id | The permanent player ID (UUID). Does not change on guest upgrade |
player.is_guest | Whether the account is still a guest |
tokens.session_token | An opaque session token. Can also be used with Authorization: Bearer instead of the cookie |
tokens.refresh_token | Only for /v1/auth/refresh and /v1/auth/logout |
tokens.access_token | A short-lived (15 min) EdDSA JWT; it is for offline verification by your game server. See Server-side verification |
tokens.expires_in_secs | The lifetime of the access JWT (seconds) |
is_new_player | true if the call created a new player |
Warning
In browser games, do not store the session and refresh tokens in JavaScript; the cookies are
HttpOnly, and that is what they are for. The SDKs do not put token bodies in state, logs, or errors.
Rate limits
The auth endpoints are subject to fixed-window rate limits. When exceeded, 429 and the plain text rate limited are returned; there is no Retry-After header: you must wait out the window yourself. Do not resend a failed attempt immediately.
| Limit | Key | Value |
|---|---|---|
| Opening a guest (including re-sign-in) | IP | 30 / 60 s |
| Email sign-in | IP | 5 / 60 s |
| Failed email sign-in lock | address + IP | 10 / 15 min (while locked, even the correct password gets 429; a successful sign-in refunds the allowance) |
| Sign-up | IP · address | 10 / 60 s · 5 / 15 min |
| Guest upgrade | IP · guest player · address | 10 / 60 s · 30 / 60 s · 5 / 15 min |
| Password change | IP · account | 10 / 60 s · 6 / 60 s |
| Password reset request (forgot) and magic link | IP · address | 5 / 60 s · 5 / 15 min |
| Password reset (reset) | IP | 5 / 60 s (shared bucket with forgot and magic link) |
| OAuth start | IP | 30 / 60 s |
| OAuth callback | IP | 60 / 60 s |
| Display name change | player | 10 / hour |
If there is an extra proxy in front of the server and the trusted proxy addresses have not been declared, all players fall into a single IP bucket. See Architecture and hosting.
Endpoint summary
| Method and path | Session | Description | Page |
|---|---|---|---|
GET /v1/auth/csrf | not required | Generates a CSRF token | Sessions and CSRF |
POST /v1/auth/sign-in/guest | not required | Guest sign-in | Guest |
POST /v1/auth/sign-up/email | not required | Sign-up | |
POST /v1/auth/sign-in/email | not required | Sign-in | |
POST /v1/auth/guest/upgrade/email | guest session | Upgrades a guest to email | |
POST /v1/auth/change-password | required | Changes the password; all sessions close | |
POST /v1/auth/forgot-password | not required | Requests a reset email | |
POST /v1/auth/reset-password | not required | New password with a token | |
POST /v1/auth/sign-in/magic-link · GET /v1/auth/magic-link/verify | not required | Magic link | |
GET /v1/auth/oauth/providers | not required | Providers enabled in the environment | OAuth |
POST /v1/auth/sign-in/oauth/{provider} | not required | Starts OAuth ({url, state}) | OAuth |
GET /v1/auth/callback/{provider} · POST (form) | not required, or required for link | Completes OAuth | OAuth |
POST /v1/auth/native/{provider} | not required | Native platform proof | OAuth › Native |
POST /v1/auth/link/{provider} | required | Starts linking an identity | Account linking |
POST /v1/auth/unlink/{provider} | required | Unlinks an identity | Account linking |
GET /v1/auth/identities | required | Linked identities | Account linking |
GET /v1/auth/session | required | Session and profile | Sessions |
POST /v1/auth/session/display-name | required | Changes the display name | Sessions |
POST /v1/auth/account/delete | required | Deletes the player’s own account (irreversible) | Account deletion |
POST /v1/auth/refresh | refresh token | Refreshes the session (rotation) | Sessions |
POST /v1/auth/logout | refresh token | Closes the session | Sessions |
GET /v1/auth/jwks | not required | JWT verification keys | Server-side verification |
The /v1/auth/organization/* endpoints (player organizations) are being deprecated: they work, but every response carries a Deprecation header and they are not described in this document. The /v1/auth/admin/* endpoints are for player administration with a server key (list, ban, rename, session revocation) and are listed in the endpoint reference.
Banned accounts
When an operator bans a player, all of the player’s sessions are revoked and their open realtime connection closes. How the player appears at sign-in:
- A banned email account with the correct password:
403and{"code":"account_banned","message":"account banned"}. A wrong password or an unknown address is an indistinguishable plain-text403; the ban status does not leak. - A banned guest: if a guest sign-in is made with the same bootstrap secret, or by presenting the banned player’s old session,
403 account_bannedis returned again and no new player is created. GET /v1/auth/sessionreturns401for a banned account (the ban code does not leak); refresh, the OAuth callback, and guest upgrade are rejected indistinguishably for a banned owner.- The response contains no ban reason or end time.
- SDK behavior: in TypeScript the session state becomes
banned; in UnitySessionStatus.Banned. Neither opens a new guest, and neither deletes the stored guest secret (so a banned player cannot escape by opening a new guest). Show the player that the account is suspended; do not build a retry loop.