Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sign in with Google

Sign in with Google lets a player sign in on the web with one click. This page describes the two steps of the flow (start and complete), what your callback page must do, the rule for automatic linking to an existing email account, and the HTTP contract of native platform proofs, which have no SDK.

Note

Today the SDK helpers for the web are only for Google. The server also has an Apple form callback and a generic provider registration, but there is no SDK helper for them and no detail in this document.

Prerequisite: configure the provider

Google works only in environments where it is configured. The operator (the super admin in the panel) enters the Google client ID, the client secret, and the redirect base on the Game settings › Sign-in methods › Google › Edit OAuth credentials screen; the secret is write-only and cannot be read again. If it is not configured, GET /v1/auth/oauth/providers returns an empty list and the start request gets 403.

The Authorized redirect URI in the Google Cloud console must be exactly the same as the redirect_uri the server uses: <redirect base>/google. For example, if the base is https://game.example.com/auth/callback, you register https://game.example.com/auth/callback/google with Google. This address is your own game page (the callback page below). The player returns there from Google with code and state. The base itself must not end with /google: if https://game.example.com/auth/callback/google is typed, the server builds …/google/google (Google answers redirect_uri_mismatch); the panel and the API refuse that value and show the corrected example. Configuration details belong to the operator; panel help: Sign-in methods.

Flow

  1. The game page gets {url, state} with POST /v1/auth/sign-in/oauth/google and redirects the player to url.
  2. The player grants consent at Google; Google returns the player to <redirect base>/google?code=…&state=….
  3. The callback page completes the session with a GET /v1/auth/callback/google?code=…&state=… call. The response is the shared sign-in envelope and writes the session cookies.

1. Start

POST /v1/auth/sign-in/oauth/google
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"callback_url":"https://game.example.com/auth/callback/google"}
{"url":"https://accounts.google.com/o/oauth2/v2/auth?client_id=…&state=…&code_challenge=…","state":"<64 hex>"}
  • url carries PKCE (code_challenge), state, and an OIDC nonce; state is valid for 10 minutes.
  • callback_url is required (400 if empty), but it does not determine the redirect: the redirect_uri sent to Google comes from the base configured on the server. Provide an address consistent with your game page.
  • 403: the provider is unknown, not configured, or disabled. 429: 30 / 60 s per IP.

2. Complete

GET /v1/auth/callback/google?code=<code>&state=<state>
x-crx-env: <environment-id>
cookie: <session cookie if any>

The response is 200 and the sign-in envelope + cookies. In the browser, use fetch(..., { credentials: "include" }); the call goes either to the same origin as the API or, if the game’s domain is in the environment’s domain list, directly to the Open Play address (CORS; see Architecture and hosting). Because it is a GET, it does not require a CSRF token.

StatusMeaning
403state missing/consumed/expired, provider rejection, or a signature/nonce error; in the link intent, no session or another player’s session. The distinction does not leak
409already exists: account_already_linked: the Google identity is linked to another player; there is no merging
422unprocessable: provider_already_linked: this provider is already linked on the player
429Callback limit: 60 / 60 s per IP

code and state are single-use; do not call the callback a second time.

TypeScript SDK

The SDK binds state to the browser for you: at start it writes {state, provider, intent, startedAt} to sessionStorage (crx-oauth-pending:v1), and at completion it checks it in the same tab. If there is no record, it is older than 10 minutes, it is corrupt, or the provider/state do not match, no network request is sent at all and the session state does not change; the CrxError code is OAuthBindingCode.Missing (missing/expired/corrupt) or OAuthBindingCode.Mismatch (another browser/tab). This prevents session takeover with someone else’s callback link.

import { CrxError, OAuthBindingCode, type PlayerClient } from "@ferdiunal/open-play-ts";

// Google'ın kullanıcıyı döndürdüğü sayfa: sunucuda yapılandırılan yönlendirme tabanı + "/google" ile aynı yol olmalıdır
// (örn. taban https://oyun.example.com/auth/callback ise sayfa https://oyun.example.com/auth/callback/google).
const CALLBACK_PATH = "/auth/callback/google";

/** 1. adım: Google'a yönlendirin. State'i saklamanız gerekmez; SDK onu bu sekmeye bağlar (sessionStorage). */
export async function beginGoogleSignIn(client: PlayerClient): Promise<void> {
  const callbackUrl = `${window.location.origin}${CALLBACK_PATH}`;
  const { url } = await client.session.startGoogleOAuth(callbackUrl);
  window.location.assign(url);
}

/** Misafir (ya da kayıtlı) oyuncunun hesabına Google kimliğini bağlar; tamamlama aynı callback sayfasında yapılır. */
export async function beginGoogleLink(client: PlayerClient): Promise<void> {
  const callbackUrl = `${window.location.origin}${CALLBACK_PATH}`;
  const { url } = await client.session.linkOAuth("google", callbackUrl);
  window.location.assign(url);
}

/** 2. adım: Google'ın döndüğü sayfada (`/auth/callback/google`) code ve state ile oturumu tamamlayın. */
export async function finishGoogleSignIn(client: PlayerClient): Promise<string> {
  const params = new URL(window.location.href).searchParams;
  const code = params.get("code");
  const state = params.get("state");
  history.replaceState(null, "", window.location.pathname); // code ve state adres çubuğunda kalmasın
  if (code === null || state === null) throw new Error("Callback adresinde code ya da state yok.");
  try {
    const session = await client.session.completeGoogleOAuth(code, state);
    return session.playerId;
  } catch (error) {
    if (error instanceof CrxError && error.code === OAuthBindingCode.Mismatch) throw new Error("Giriş başka bir tarayıcıda başlatılmış.");
    if (error instanceof CrxError && error.code === OAuthBindingCode.Missing) throw new Error("Giriş süresi dolmuş; yeniden başlatın.");
    throw error;
  }
}

/** Hesabıma bağlı sağlayıcı kimlikleri (sağlayıcı hesap kimliği ve e-posta dönmez). */
export async function linkedProviders(client: PlayerClient): Promise<string[]> {
  const result = await client.session.listIdentities();
  return result.identities.map((identity) => identity.provider);
}
  • startGoogleOAuth(callbackUrl) = startOAuth('google', callbackUrl); completeGoogleOAuth(code, state) = completeOAuth({ provider, code, state }).
  • linkOAuth(provider, callbackUrl) starts the link flow (see below); completion is again with completeOAuth.
  • listIdentities() returns the linked identities.
  • On the callback page, remove code and state from the address bar right away (history.replaceState).
  • The Unity SDK has no OAuth helper.

Warning

A method named startIdentityTransfer appears in the TypeScript SDK; it has no counterpart on the server (operator-assisted identity transfer was cancelled). Do not use it.

Linking guest progress to Google

For a player with an open session (guest or registered), linkOAuth is used: POST /v1/auth/link/{provider} requires a session and returns {url, state}; completion is the same callback endpoint, but the callback request must come with the session of the player who started it (a cookie or Bearer), otherwise 403.

  • If the player is a guest, linking is an upgrade: the same player.id, is_guest: false, the guest secret is retired, and a new session is opened. A name the guest set themselves is kept; if the name is still the default Guest, the Google name is applied.
  • If the player is registered, only the new identity is added; their name is not touched.
  • There is one identity per provider (422 provider_already_linked).
  • If the Google identity is linked to another player, 409 account_already_linked is returned; progress is not merged.

Details: Account linking.

Automatic linking by email

If the email of a player who signs in with Google with a new identity matches an existing email/password account, the Google identity is automatically linked to that account and the player enters that account. The conditions of the rule:

  • The email must be verified by Google and Google must vouch for the current ownership of this email: the domain is gmail.com/googlemail.com, or the hd claim of the signed ID token equals the email’s domain (Google Workspace). For a third-party address, email_verified alone is not enough.
  • The match is made with the player’s profile email (players.email); the password, existing sessions, and the profile email are not touched.
  • If the matched account is banned, the link is not written and the sign-in is rejected.
  • If the matched account already has another Google identity, automatic linking is not done; a new player is created.
  • It works even if sign-up is disabled (it is not a new sign-up).
  • There is no setting that turns this behavior off; this linking is not applied on the native ID-token path.
  • When a guest links to Google (upgrade), if the Google email matches another player’s email, no merge is done; 409 account_already_linked is returned.

What is stored on the account: the Google identity is recognized only by (environment, provider, sub). Google’s name, verified email, language, and profile image address may be written to the identity; the provider’s access and refresh tokens are not stored. This information is visible only in the operator panel.

Native platform proofs

There is a single endpoint for native mobile sign-ins; there is no SDK helper yet (not in TypeScript, not in Unity). The endpoint is subject to the shared CSRF rules and does not write cookies: use the response as a Bearer.

POST /v1/auth/native/{provider}
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>
providerBody
apple, google{"id_token":"<OIDC ID token>","nonce":"<raw nonce>"}
gamecenter{"proof":{"player_id":"…","bundle_id":"…","public_key_url":"…","signature_b64":"…","salt_b64":"…","timestamp_ms":1700000000000,"game_player_id":"…"}}
playgames{"proof":{"server_auth_code":"…","application_id":"…"}}

200 and {player, tokens, is_new_player} (no cookies). 400: the provider is not suitable for native sign-in or is not registered in the environment, or the envelope format is broken. 403: the proof was rejected (no distinction leaks, such as signature, certificate chain, bundle ID, timeout, or nonce). To use it, the relevant platform must be configured in the environment.

Note

The native proof path does not apply automatic linking by email; the same person can be split into separate players by the native and web paths.