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

Verifying the player on your game server

Your game server wants to know which player an incoming request or connection belongs to. This page describes two paths: verifying the access JWT that Open Play signed offline (with the JWKS) and verifying by asking Open Play about the session. The comparison table at the end of the page tells you which path suits you.

Warning

Today’s SDK limit: The TypeScript and Unity SDKs do not expose the access JWT to the application; the token exists only in the body of the raw REST sign-in response (tokens.access_token). A client that uses an SDK cannot give the JWT to your server. For SDK-based clients, Path B below (session verification) can be used. This gap is on record.

Path A: verify the access JWT offline

The token

The sign-in, sign-up, refresh, and OAuth callback responses return tokens.access_token: a compact JWT signed with EdDSA (Ed25519), valid for 15 minutes. The token is only in the body; it is not carried in a cookie. When it expires, POST /v1/auth/refresh gives a new one.

Header: {"alg":"EdDSA","typ":"JWT","kid":"<key-id>"}. Payload:

FieldMeaning
subPlayer ID (UUID)
envEnvironment ID (UUID)
sidSession ID (UUID)
orgActive organization (the field is absent if there is none)
iat, expIssue and expiry time: Unix milliseconds (not seconds)

Warning

iat and exp are milliseconds, and the token has no iss/aud. Standard JWT libraries count exp in seconds; they mistake the millisecond value for a date in the far future and accept an expired token. If you use a library, have it verify the signature but do the exp check yourself (exp > Date.now()).

Keys (JWKS)

curl -sS "$OPEN_PLAY_URL/v1/auth/jwks"
{"keys":[{"kty":"OKP","crv":"Ed25519","kid":"<key-id>","use":"sig","alg":"EdDSA","x":"<base64url public key>"}]}

No credentials are needed. If the server has not published the signer, 404 is returned. Cache the response and refresh it only if the kid in the token is not in the list; do not fetch it on every request.

Verification steps

  1. Split the token into three parts; the alg value in the header must be EdDSA, otherwise reject it.
  2. Find the key in the JWKS by kid; if it is missing, refresh the JWKS once.
  3. Verify the signature over the header.payload text with Ed25519.
  4. Check that exp > now (ms).
  5. Compare the env value with your own environment ID.

Warning

The tokens of all games on the same Open Play server are verified with the same JWKS. If you skip the env comparison, a player of another game appears valid on your server. This step is mandatory.

Node.js (only the built-in node:crypto, no dependencies):

import { createPublicKey, verify, type JsonWebKey } from "node:crypto";

/** Open Play erişim JWT'sinin (EdDSA) yük alanları. `iat` ve `exp` saniye değil, Unix MİLİSANİYEDİR. */
export interface AccessClaims {
  readonly sub: string; // oyuncu kimliği
  readonly env: string; // ortam kimliği
  readonly sid: string; // oturum kimliği
  readonly org?: string;
  readonly iat: number;
  readonly exp: number;
}

interface Jwks {
  readonly keys: ReadonlyArray<JsonWebKey & { readonly kid?: string }>;
}

function decode(part: string): Buffer {
  return Buffer.from(part, "base64url");
}

/**
 * Oyuncunun erişim JWT'sini çevrimdışı doğrular: imza, süre ve ortam eşleşmesi.
 * JWKS'i her istekte çekmeyin; `jwks` parametresini önbellekten verin (anahtar döndürülürse bilinmeyen `kid` gelince yenileyin).
 */
export function verifyAccessToken(token: string, jwks: Jwks, expectedEnvironmentId: string, nowMs: number = Date.now()): AccessClaims {
  const parts = token.split(".");
  const [header, payload, signature] = parts;
  if (parts.length !== 3 || header === undefined || payload === undefined || signature === undefined) {
    throw new Error("Biçimi bozuk JWT.");
  }
  const head = JSON.parse(decode(header).toString("utf8")) as { alg?: string; kid?: string };
  if (head.alg !== "EdDSA") throw new Error("Beklenmeyen algoritma.");
  const jwk = jwks.keys.find((key) => key.kid === head.kid);
  if (jwk === undefined) throw new Error("Bilinmeyen anahtar kimliği; JWKS'i yenileyin.");
  const publicKey = createPublicKey({ key: jwk, format: "jwk" });
  const signed = Buffer.from(`${header}.${payload}`, "utf8");
  if (!verify(null, signed, publicKey, decode(signature))) throw new Error("İmza doğrulanamadı.");

  const claims = JSON.parse(decode(payload).toString("utf8")) as AccessClaims;
  if (claims.exp <= nowMs) throw new Error("Belirtecin süresi dolmuş.");
  // JWKS tüm oyunlar için ortaktır: başka bir ortamın oyuncusunu kabul etmemek için `env` mutlaka karşılaştırılır.
  if (claims.env !== expectedEnvironmentId) throw new Error("Belirteç başka bir ortama ait.");
  return claims;
}

/** JWKS'i çeker (kimlik gerekmez). */
export async function fetchJwks(baseUrl: string): Promise<Jwks> {
  const response = await fetch(`${baseUrl}/v1/auth/jwks`);
  if (!response.ok) throw new Error(`JWKS okunamadı: ${response.status}`);
  return (await response.json()) as Jwks;
}

Limits

  • It does not see revocation. Offline verification cannot know that the session was closed, revoked, or that the player was banned; the token looks valid until its exp (at most 15 minutes). For operations that must be revocation-aware, use Path B or accept this 15-minute window.
  • Key rotation. A key change restarts the server; the new kid appears in the JWKS. Your cache must refresh when it sees an unknown kid.

Path B: ask Open Play about the session

You resolve the player by forwarding the player’s session token (the cookie value or the Authorization: Bearer token) and the environment ID to Open Play. In a browser game, requests that go to the game’s origin already carry the session cookie (__Secure-crx.session); your server can read this value and forward it to Open Play as a Bearer.

curl -sS "$OPEN_PLAY_URL/v1/auth/session" \
  -H "Authorization: Bearer $PLAYER_SESSION_TOKEN" \
  -H "x-crx-env: $OPEN_PLAY_ENV"

200 and {"environment_id","player_id","session_id","is_guest","displayName","email",…}. A revoked, expired, or banned session gives 401. Because it is a GET, no CSRF token is needed.

Warning

The session token is an authority. Do not write it to plain-text logs, do not spread it to other services, and forward it only over TLS.

Which path should I choose?

Path A: offline JWTPath B: session query
Request to Open PlayNone (the JWKS is cached)One request per verification
Sees revocation/banNo (up to 15 min delay)Yes
Token sourceOnly the raw REST sign-in response; the SDK does not provide itThe cookie or Bearer session token; also works with an SDK-based client (the cookie in the browser)
Environment verificationYou compare the env claimThe environment is checked against the x-crx-env in the request
When?Very high request volume, a revocation delay is acceptableThe default recommendation; operations where player identity is critical

For server-to-server calls, a server key is used instead of the player (Concepts).