Accounts & authentication
Tokens#
A login answers an AuthSession: the account and a token pair.
{
"account": {
"id": 42,
"email": "player@example.com",
"email_verified": false,
"display_name": "Player One",
"roles": [],
"identities": [],
"created_at": 1790000000000
},
"tokens": {
"token_type": "Bearer",
"access_token": "nbsa_…",
"access_expires_at": 1790003600000,
"refresh_token": "nbsr_…",
"refresh_expires_at": 1792592000000
}
}
| Token | Lifetime (default) | Use |
|---|---|---|
| access token | 1 hour (access_expires_at) |
Authorization: Bearer <access token> on HTTP requests and the WebSocket handshake, or the WebSocket auth message |
| refresh token | 30 days (refresh_expires_at; every refresh starts a new one) |
POST /v1/auth/refresh only |
- Tokens are opaque strings (today
nbsa_/nbsr_plus 64 hex characters). Store and send them unchanged; never parse them. Use the*_expires_atfields, not the token text, for timing. - Refresh before expiry: when less than 60 seconds of the access token are left, refresh.
- Rotation: every refresh token works once and is replaced by the answer's new pair.
Presenting the same refresh token again within 30 seconds of its first use (an HTTP retry,
two tabs refreshing at once) answers the same new pair. Presenting it again later revokes
the whole session (
refresh_token_reused: the token was probably stolen), and the player must log in again. So: refresh single-flight, store the new pair before using it, keep the old pair until the new one is stored, and share tokens between tabs (see the checklist). - Sessions: one per login. Logout revokes one session or all of them; a password change revokes the other sessions; a password reset or a ban revokes all. Revocation takes effect on the next request; open WebSockets of a revoked session are closed (4001, or 4003 for a ban).
- Expired or revoked tokens: an expired access token answers 401
token_expired(refresh and retry); an unknown, malformed or revoked one 401unauthorized; a banned account's token or refresh 403banned. Routes that do not need a caller (register, login, refresh, logout, forgot / reset, verify) ignore a staleAuthorizationheader, so a client that always sends its last token still works there.POST /v1/auth/steamis the exception: there a Bearer token that is invalid or expired is refused.
Register, log in, refresh, log out#
| Step | Request | Answer |
|---|---|---|
| register | POST /v1/auth/register {"email","password","display_name"?} |
AuthSession; a verification mail is sent |
| log in | POST /v1/auth/login {"email","password"} |
AuthSession |
| refresh | POST /v1/auth/refresh {"refresh_token"} |
TokenPair |
| log out this session | POST /v1/auth/logout {} with the Bearer token, or {"refresh_token":"…"} when the access token expired |
{} |
| log out everywhere | POST /v1/auth/logout {"everywhere":true} (+ Bearer or refresh_token) |
{} |
Login answers the same 401 invalid_credentials for an unknown address and a wrong password. A
server may require a verified address for password logins (403 email_not_verified) or close
registration (403 forbidden).
Email verification and password reset (website pages)#
Mails contain a one-time token, or a link the operator configured with the token in it, for
example https://your-site.example/verify?token=… and https://your-site.example/reset?token=….
A website implements those two pages:
| Page | What it does |
|---|---|
/verify?token=… |
POST /v1/auth/email/verify {"token":"<token from the URL>"} → {}, or 400 invalid_token (unknown, used or expired: offer "send again", which is POST /v1/auth/email/resend with the player's Bearer token) |
/reset?token=… |
a form for the new password, then POST /v1/auth/password/reset {"token":"…","new_password":"…"} → {}; every session of the account is revoked, so log in afterwards |
| "forgot password" form | POST /v1/auth/password/forgot {"email":"…"} → always {} (whether or not the address has an account) |
Verification tokens expire after 24 hours, reset tokens after 1 hour; both are single-use. A reset also unlinks login providers (Steam) by default.
Steam#
A game gets a ticket with Steamworks' GetAuthTicketForWebApi(identity) and posts it hex-encoded
with the identity string: POST /v1/auth/steam {"ticket_hex":"…","identity":"…"} → AuthSession.
The server checks the ticket with Steam; the first login creates an account (its email is
null; identities holds {"provider":"steam","subject":"<SteamID64 as a string>"}). With the
Bearer token of a login younger than 10 minutes, the same request links Steam to that account
instead (otherwise 403 reauthentication_required). The route answers 404 when the server has no
Steam login configured. Steam login is for game clients with the Steam SDK; web pages use email +
password.
Bans#
A banned account gets 403 banned on login, refresh and every authenticated request, with
details.until (unix ms) for a timed ban. Its sessions are revoked and its WebSockets closed with
4003. Show the ban and do not retry automatically.
Roles#
Roles are strings in account.roles (admin, moderator, a game's own); a normal player has
none. The admin routes need admin; deleting other players' chat messages needs admin or
moderator (server setting).
Generated from API.md (repository commit e25edb9, file sha256 3db87f7bba70).