Skip to main content

Authentication

Signing in sets sb_admin_session: HttpOnly, Path=/admin, SameSite=Strict.

The token never appears in a response body, and Authorization: Bearer is not accepted on this surface. An XSS that can read a token can impersonate an operator indefinitely; one that can only use a cookie is bounded by the browser.

The domain constraint decides your DNS

SameSite=Strict reasons about the registrable domain, not the origin.

admin.slotbattle.example.com and api.slotbattle.example.com are cross-origin but same-site, and work. console.example.net and api.example.com do not.

Get it wrong and every authenticated request fails silently: the browser never attaches the cookie, nothing logs an error, and the requests come back unauthenticated.

Decide this before buying domains. It cannot be worked around from application code.

CORS matters here too. Credentials are only allowed for a named origin, never for *, and an empty origin allowlist means the browser refuses to send the cookie at all.

Three ways in

Password, optionally with a TOTP code when the account has two-factor enabled.

Magic link, a single-use code emailed to the address.

Password reset, the same mechanism with a different outcome.

Both email routes answer 202 unconditionally

They answer 202 whether or not the address exists, which is what stops them being used to enumerate accounts. It also means undelivered mail is invisible from the API: if mail is unconfigured, the route still answers 202 and nothing arrives.

slotbattle:config-check is what catches that. The API will not.

Two-factor

Enrol, then activate with a code. Activation returns recovery codes, shown once, under the same rule as API keys.

Disabling TOTP is a DELETE with a body carrying the password. The alternative, POST /totp/disable, describes the effect less accurately.

invalid_code means three things at once

Wrong, expired and already-used all answer invalid_code. There is no code_expired.

Distinguishing them would tell someone probing a magic link that they had the right code but were too late.

Switching casinos

The active casino travels in X-Tenant-Id, not in the session token, which is what lets an operator with access to several casinos switch without re-authenticating.

Membership is checked on every request, so the header states an intent rather than an entitlement. Naming a casino you are not a member of is refused.

GET /admin/tenants is the one collection reachable without the header: it draws the casino switcher, so requiring a casino would make choosing one depend on having chosen one.

Throttling

Every unauthenticated route is throttled inside its handler. Sustained failures answer too_many_requests.

Common failures

SymptomCause
Every login answers 500, correct password includedThe JWT secret is empty
Login succeeds, every later request is unauthenticatedConsole and API on different registrable domains
Browser refuses to send the cookieOrigin allowlist empty, or the cookie's secure flag set over plain HTTP
Magic links never arriveMail unconfigured. The API cannot report this, so run the config check