quest'n'click 4.5.0 · r1

Built for quest'n'click 4.5.0, revision r1. Whether it is published, and which one is current, is recorded at publication.json.

Security

Français · Type: explanation + reference · Audience: admin · Context: hosted Studio · Status: ready · Applies from: 4.2.3

The Studio Server (hosted team Studio) · contract: experimental · verified by CI (support matrix of 4.4.0, 2026-10-23): Postgres, S3 and Keycloak in CI; Chromium and Firefox (job studio-hosted); WebKit on macOS, by hand only · automated checks: jobs studio-hosted, studio-install-smoke; CI plan studioHosted; 3 test files · people's passes: studio-hosted not run (#149), studio-collab not run (#150), studio-preview not run (#166), studio-assets-hosted not run (#167), studio-mcp-remote not run (#196), studio-oidc-generic not run (#197) · known limits: worker-network

How a hosted Studio on Clever Cloud decides who a request is, what it may do, how fast a removed member loses access, and why the game's code never touches a secret. Each rule here is read from the current code; its limits are said.

Why

A shared Studio holds a team's work and runs code a member wrote. Three dangers shape it: a page of another site acting with a member's session (CSRF), a member who should no longer get in, and the game's own code reaching the Studio's secrets. The provider says who someone is; the Studio decides what they may do (ADR 0027).

The HTTP policies

Each request gets exactly one policy, chosen by the request alone; never two identities on one request.

PolicyWhenRules
A, cookiean API request with no Authorization headerThe cookie __Host-ws_session (Secure, HttpOnly, SameSite=Lax); only its sha256 is stored. A mutation (POST, PUT, PATCH, DELETE) needs Origin equal to publicUrl, X-Studio-Request: 1 and a JSON body; a refusal says only forbidden (403)
B, Beareran Authorization headerAn access token (a JWT) for the configuration's audience, 5 min by default (15 at most); cookies ignored. An id_token, another audience, a longer token, a non-member, a foreign Origin: refused
C, callbackGET /auth/callbackThe provider's answer to a login begun server-side (Authorization Code, PKCE S256, state, nonce; the pre-authentication used once, 10 min)
D, back-channel logoutPOST /auth/backchannelA form-encoded logout_token, checked per the specification; a replay refused by its jti
E, login and logoutGET /auth/login, POST /auth/logoutThe login's return path is relative only, never another site
F, the game's originany request on playUrlA ticket from a member's browser session opens a game session (cookie Secure; HttpOnly; SameSite=None; Partitioned); without it, 401 "open the game from the Studio", before the path is read. No API, no /auth, no /metrics there

Roles live in the Studio: reading needs a reviewer, writing a dev, the members and the audit log an owner. The last owner can be neither demoted nor removed.

Revocation, R = 15 minutes

EventWhen access ends
A member removed or revoked in the StudioAt their next request: sessions deleted, every token issued before refused (revoked_before); their long connections (live stream, MCP) close within 5 s, their leases are released
A back-channel logout from the providerAt once: with a sid, that provider session only; with a sub alone, every session and token of the user
Disabled at the provider, no back-channelAt the session's next check: every 5 min, by its refresh token; a definitive refusal ends it
The provider unreachableThe session lives until R (15 min) after its last confirmation, then is suspended (503, fail closed) until a check succeeds
A provider that gives no refresh tokenSessions of R at most
A refresh token that no longer opens (its key left the ring)The session ends: log in again (Rotating keys)

Browser sessions also end after 8 h idle or 24 h in all (sessions.idleHours, sessions.absoluteHours).

Guarantee · provider-unreachable-suspends · referenced · executed on aff6caab7514 · scope self-checked by the author (AI), not reviewed When the provider cannot be reached for longer than R, a session is suspended: the Studio answers 503 rather than serving it unchecked, and the session comes back once the provider answers a check. The test runs on a simulated clock, against a test provider.

The worker without secrets

The game's code (its validator, solver, previews, migration, asset tools) runs only on studio-worker, a second app (ADR 0028). On Clever Cloud's Node.js runtime there is one system user, and a child process can read its parent's environment through /proc (spike S §1): so the worker holds nothing worth stealing.

Limit · worker-network The worker's outbound network is not restricted: the game's code running there could reach the Internet. Running the worker without outbound network was not tried (QUALIF-4.2.3 §7).

Behind Clever's proxies: trustProxy

The server believes X-Forwarded-For and X-Forwarded-Proto only from the addresses in trustProxy; with OIDC and listening beyond the loopback it refuses to start without them. That is how two members behind Clever's proxies are told apart for the rate limits (the /auth/ routes: 30 requests a minute per address, per instance, in memory; then 429). The private ranges of the example are an assumption to narrow (Configuration).

Secrets

Read once from <NAME>_FILE (0600) or the environment, then removed from the process's environment; never logged. Refresh tokens and id_tokens are sealed with AES-256-GCM under the STUDIO_SESSION_KEY ring. A backup is trusted input (it can add owners). The full list: Configuration.

The game's cookie and the domains

With publicUrl and playUrl on two subdomains of one domain, the Studio and the game are one site and the game's frame keeps its cookie in every browser. Two *.cleverapps.io names are two sites, where the cookie survives only as a partitioned one (CHIPS), which WebKit on Linux does not keep: Domains and TLS.

Alternatives set aside

Trade-offs

Decisions and ADRs


Edit the source of this page · Report an error