feat(registry): an entitlement ticket verifies once #23

Merged
kblack0610 merged 1 commit from feat/ticket-used-once into main 2026-11-05 16:08:23 +00:00
Owner

A ticket could be presented by any connection until it expired (playground #1715, docs/operations/premium-content.md, Open). POST /api/v1/entitlements/verify now spends it.

  • used_tickets (migration 0009) holds the signature of each verified ticket. The insert decides a race, so one of two verifies wins. Rows of expired tickets are cleared on each verify, so no new sweep.
  • Tickets carry a nonce: without one, two tickets made in the same second are the same string and the second would read as used.
  • A verify refused for its session or lease does not spend the ticket. The check runs before recordSessionAccount, so a replay records nothing.

assumed: single use rather than an account match, because a game server knows no account for a connection except the one in the ticket, and a client-sent id can be read out of the ticket and forged.

Not closed: a modded client can still give a friend an unspent ticket, one friend per ticket.

Tested: go test ./... green in golang:1.24 with Postgres and MinIO, gofmt and vet clean. TestTicketVerifiesOnce went red with the used check disabled.

Removed: nothing; this adds a check the route did not have.

Live registry is v1.11.0, which has no entitlement routes yet, so this rides the pending premium release. No tag or deploy here.

https://claude.ai/code/session_01BALUC3rdg9oGzE5xsFNU2D

A ticket could be presented by any connection until it expired (playground #1715, docs/operations/premium-content.md, Open). `POST /api/v1/entitlements/verify` now spends it. - `used_tickets` (migration 0009) holds the signature of each verified ticket. The insert decides a race, so one of two verifies wins. Rows of expired tickets are cleared on each verify, so no new sweep. - Tickets carry a `nonce`: without one, two tickets made in the same second are the same string and the second would read as used. - A verify refused for its session or lease does not spend the ticket. The check runs before `recordSessionAccount`, so a replay records nothing. assumed: single use rather than an account match, because a game server knows no account for a connection except the one in the ticket, and a client-sent id can be read out of the ticket and forged. Not closed: a modded client can still give a friend an unspent ticket, one friend per ticket. Tested: `go test ./...` green in golang:1.24 with Postgres and MinIO, gofmt and vet clean. `TestTicketVerifiesOnce` went red with the used check disabled. Removed: nothing; this adds a check the route did not have. Live registry is v1.11.0, which has no entitlement routes yet, so this rides the pending premium release. No tag or deploy here. https://claude.ai/code/session_01BALUC3rdg9oGzE5xsFNU2D
A ticket was a bearer secret until it expired, so a second connection could present it. verify now records its signature in used_tickets and answers 401 "ticket already used" the next time. A nonce keeps two tickets made in the same second apart.

Claude-Session: https://claude.ai/code/session_01BALUC3rdg9oGzE5xsFNU2D
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kblack0610/bnb-game-servers!23
No description provided.