Identity incidents: accounts, sessions, and customer origins
The runbook for "someone is in an account they should not be in", "a customer's game is compromised", and "a credential leaked". Companion to the player-code kill ladder and commerce incidents. Everything here is a GraphQL call on the Management API or a Studio action; nothing needs a database session.
The controls at a glance
| Control | Where | Since |
|---|---|---|
| Direct sign-in is first-party only | HOSTED_SIGN_IN_REQUIRED from any other browser origin | ck-api v1.88.0 |
| CORS = first-party + every app's redirect URIs, live | OriginPolicyService, 10 s snapshot | v1.88.0 |
register never touches an account that has a password | EMAIL_ALREADY_REGISTERED | v1.87.2 |
| Reset revokes every session; change revokes the others | resetPassword / changePassword | v1.88.0 |
| Sign-in rate limits (per address, per client) | RATE_LIMITED; masked mutations stay masked | v1.88.0 |
| Authentication is the default on every root field | @Public() allow-list + AuthDefaultGuard | v1.88.0 |
| App tokens are confined to one app, 30 min | refreshAppToken; revokeAppAuthorization | unchanged |
| Studio session is an HttpOnly cookie + CSRF | ck_session / ck_csrf; cookie auth from a non-first-party origin is COOKIE_AUTH_ORIGIN_REFUSED. Prod cookie Domain is .crowdedkingdoms.com (v1.92.1) so public studio.crowdedkingdoms.com can send CSRF. | ck-api v1.92.1 |
Playbooks
A. A player reports their account was taken over
- Confirm the address with the player out of band.
- Trigger a reset:
requestPasswordReset(email)(or have the player use "Forgot password" on Studio). Completing the reset revokes every session and every app token for the account -- the attacker is out the moment the player finishes. - If the player cannot receive email, a super admin can
forceLogoutUser(userId)(every session gone) and then set a temporary password through the support path. - Check Connected apps (Account > Connected apps, or
myAuthorizedAppsas the user) for a grant the player does not recognise, and revoke it (revokeAppAuthorization). CheckmyIdentitiesfor a linked social identity the player did not add (unlinkIdentity). - Ask how the credential left them. If they typed their Crowded Kingdoms password into a game's page, that game is either a first-party page or a phishing copy: since v1.88.0 no customer game can collect it. Escalate.
B. A customer's game (origin) is compromised
What the attacker holds: app tokens for that app, minted by players who consented on Studio, and CORS access to the API from that origin. What they do not hold: passwords, sessions, tokens for other apps.
- Close the door. In Studio > Apps > Settings > Sign-in & redirect URIs,
remove the compromised origin (or every URI). Within ten seconds on every
API instance the origin gets no CORS headers and hosted sign-in refuses to
return there. Equivalent:
setAppClientSettings({ appId, redirectUris: [] }). - Invalidate what is out there. Each player's grant can be revoked
(
revokeAppAuthorization(appId)as that user invalidates their live tokens for the app). For a fleet-wide cut, an app manager canarchiveApp(appId)so nothing is served for it; tokens also expire on their own within 30 minutes and cannot be refreshed once the grant is gone. - Notify the customer and, if players were affected, the players (the
account address is in
users). - Re-open by re-adding the origin once the customer has remediated.
C. A sign-in flood / credential stuffing
- Read the per-minute summary in the API logs:
auth.rate_limited op=<login|register|...> dimension=<email|ip> origin=<origin> refused=<n>, anddirect sign-in refused: field=<f> origin=<o>for non-first-party attempts. - The limiter is per datacenter and per address / per client. A legitimate
population behind one NAT trips the IP dimension at 100 attempts per
15 minutes; if that is what you see (successful logins refused), raise
AUTH_RATE_LIMITS.login.ipin a release rather than disabling the limiter. - A flood against one address locks that account for 15 minutes after ten failures; the owner's way in is the reset email, which also clears the window. Nothing to do unless the owner asks.
- Sustained floods are a WAF question (Phase 3, not yet in place).
D. A credential leaked (token, key, password)
| Leaked | Do |
|---|---|
A player's session token (or a stolen ck_session) | forceLogoutUser(userId) (super admin) or have them logoutAllDevices; then a password change. Studio no longer keeps the session in localStorage; XSS there cannot read ck_session. |
| An app token | Expires within 30 min; revokeAppAuthorization ends it now. |
The P2P_SECRET / a control-plane secret | Rotate in Secrets Manager and re-file the component (infra-control-plane/docs/ops/SECRETS.md). |
| An org API token | Org > API credentials > revoke; it stops authenticating on its next request. |
| A password in a log or chat | Treat as A; the owner resets. |
E. A customer asks why their game gets HOSTED_SIGN_IN_REQUIRED or CORS errors
Both have one fix: the game's origin is not one of the app's redirect URIs.
Studio > Apps > Settings > Sign-in & redirect URIs; or npm run setup -- --origin https://their.host in The Construct. Direct auth.login from a
game page is not supported on any tier; the game uses
client.portal.signIn (Sign in).
What to expect from the logs
- One
WARNper refused direct sign-in naming the field and origin (FirstPartyOriginGuard). A first-party page appearing here means its host is missing fromFIRST_PARTY_ORIGINS/FRONTEND_URLon that tier. - One
WARNper minute per (op, dimension, origin) from the rate limiter, with the count, never one per refusal. AuthDefaultGuardwarns once per process for a root field that is neither@Public()nor behindTokenAuthGuard; the build also fails, so seeing it on a tier means a gate was bypassed.