mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-15 13:52:02 +00:00
feat: wire durable session authentication into API
This commit is contained in:
+1
-1
@@ -1135,7 +1135,7 @@ New `--role=client-reconnect` grades the returning player: not a spectator, owns
|
|||||||
| 7.3 `[D:7.2]` `[P]` | `server_browser.tscn` via `ISteamMatchmakingServers` | Internet, LAN, favourites and history lists all populate |
|
| 7.3 `[D:7.2]` `[P]` | `server_browser.tscn` via `ISteamMatchmakingServers` | Internet, LAN, favourites and history lists all populate |
|
||||||
| 7.4 `[D:7.2]` `[P]` | Auth tickets in `hello` → `BeginAuthSession`; Steam identity in the roster; persistent ban list | Ownership, VAC and ban state verified server-side |
|
| 7.4 `[D:7.2]` `[P]` | Auth tickets in `hello` → `BeginAuthSession`; Steam identity in the roster; persistent ban list | Ownership, VAC and ban state verified server-side |
|
||||||
| 7.5 `[D:7.2]` `[P]` | Feature-gate every Steam call behind `OS.has_feature("steam") and ClassDB.class_exists("SteamMultiplayerPeer")`; verify the ENet path end to end | Non-Steam build is fully functional |
|
| 7.5 `[D:7.2]` `[P]` | Feature-gate every Steam call behind `OS.has_feature("steam") and ClassDB.class_exists("SteamMultiplayerPeer")`; verify the ENet path end to end | Non-Steam build is fully functional |
|
||||||
| 7.6 `[D:7.4]` | **IN PROGRESS.** Pure Go `AuthCoordinator` models pending/accepted/rejected/cancelled backend auth sessions around the single-use verifier, including maintenance expiry sweeping; `store.PostgresSessions` persists only token digests and enforces durable expiry/revocation | `server/domain/auth.go`, `server/store/session_sql.go` and adversarial tests keep identity unavailable while pending, reject cancellation/expiry/wrong-attempt/replay, expire abandoned attempts at the boundary, release identity only after verifier success, and reject invalid session inputs; real Steam BeginAuthSession/EndAuthSession adapter and live PostgreSQL/session integration remain |
|
| 7.6 `[D:7.4]` | **IN PROGRESS.** Pure Go `AuthCoordinator` models pending/accepted/rejected/cancelled backend auth sessions around the single-use verifier, including maintenance expiry sweeping; `store.PostgresSessions` persists only token digests and the authenticated API can inject that durable session backend | `server/domain/auth.go`, `server/store/session_sql.go`, `server/api/service.go` and adversarial tests keep identity unavailable while pending, reject cancellation/expiry/wrong-attempt/replay, expire abandoned attempts at the boundary, release identity only after verifier success, reject invalid session inputs, and prove API delegation; real Steam BeginAuthSession/EndAuthSession adapter and live PostgreSQL/session integration remain |
|
||||||
| 7.7 `[D:7.1]` `[P]` | Obtain the production App ID, publisher key, SDR coordinator SDK/signing approval, certificates and hosted-data-centre support from Valve | Production prerequisites and rotation owners are recorded; no Spacewar credential or development certificate can reach a release build |
|
| 7.7 `[D:7.1]` `[P]` | Obtain the production App ID, publisher key, SDR coordinator SDK/signing approval, certificates and hosted-data-centre support from Valve | Production prerequisites and rotation owners are recorded; no Spacewar credential or development certificate can reach a release build |
|
||||||
| 7.8 `[D:7.6,7.7]` | Ticketed Hosted Dedicated Server SDR: routing registration, coordinator-issued player→server relay tickets, client ticket installation, reconnect and expiry | Two real accounts complete and reconnect to an assigned dedicated match through SDR; server/player IPs are not exposed; ENet gates remain green |
|
| 7.8 `[D:7.6,7.7]` | Ticketed Hosted Dedicated Server SDR: routing registration, coordinator-issued player→server relay tickets, client ticket installation, reconnect and expiry | Two real accounts complete and reconnect to an assigned dedicated match through SDR; server/player IPs are not exposed; ENet gates remain green |
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -30,8 +30,13 @@ type QueueBackend interface {
|
|||||||
Get(context.Context, string, string, time.Time) (domain.QueueTicket, error)
|
Get(context.Context, string, string, time.Time) (domain.QueueTicket, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionBackend interface {
|
||||||
|
Authenticate(context.Context, string, string, time.Time) (domain.Session, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Sessions *domain.SessionStore
|
Sessions *domain.SessionStore
|
||||||
|
SessionBackend SessionBackend
|
||||||
Queue *domain.Queue
|
Queue *domain.Queue
|
||||||
Candidate CandidateProvider
|
Candidate CandidateProvider
|
||||||
CandidateV2 CandidateProviderV2
|
CandidateV2 CandidateProviderV2
|
||||||
@@ -330,7 +335,7 @@ func (s *Service) probe(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) authenticate(w http.ResponseWriter, r *http.Request) (string, bool) {
|
func (s *Service) authenticate(w http.ResponseWriter, r *http.Request) (string, bool) {
|
||||||
if s.Sessions == nil {
|
if s.Sessions == nil && s.SessionBackend == nil {
|
||||||
writeError(w, http.StatusServiceUnavailable, "auth_unavailable")
|
writeError(w, http.StatusServiceUnavailable, "auth_unavailable")
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -344,7 +349,13 @@ func (s *Service) authenticate(w http.ResponseWriter, r *http.Request) (string,
|
|||||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
session, err := s.Sessions.Authenticate(parts[1][:separator], parts[1][separator+1:], s.now())
|
var session domain.Session
|
||||||
|
var err error
|
||||||
|
if s.SessionBackend != nil {
|
||||||
|
session, err = s.SessionBackend.Authenticate(r.Context(), parts[1][:separator], parts[1][separator+1:], s.now())
|
||||||
|
} else {
|
||||||
|
session, err = s.Sessions.Authenticate(parts[1][:separator], parts[1][separator+1:], s.now())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||||
return "", false
|
return "", false
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import (
|
|||||||
|
|
||||||
type queueBackendSpy struct{ createCalls, heartbeatCalls, cancelCalls, getCalls int }
|
type queueBackendSpy struct{ createCalls, heartbeatCalls, cancelCalls, getCalls int }
|
||||||
|
|
||||||
|
type sessionBackendSpy struct{ calls int }
|
||||||
|
|
||||||
|
func (s *sessionBackendSpy) Authenticate(_ context.Context, sessionID, _ string, _ time.Time) (domain.Session, error) {
|
||||||
|
s.calls++
|
||||||
|
return domain.Session{SessionID: sessionID, PlayerID: "player-1"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *queueBackendSpy) Create(_ context.Context, playerID, ticketID, _ string, spec domain.QueueSpec, now time.Time) (domain.QueueTicket, error) {
|
func (b *queueBackendSpy) Create(_ context.Context, playerID, ticketID, _ string, spec domain.QueueSpec, now time.Time) (domain.QueueTicket, error) {
|
||||||
b.createCalls++
|
b.createCalls++
|
||||||
return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, Playlist: spec.Playlist, State: domain.Queued, EnqueuedAt: now, ExpiresAt: now.Add(domain.QueueExpiryWindow)}, nil
|
return domain.QueueTicket{TicketID: ticketID, PlayerID: playerID, Playlist: spec.Playlist, State: domain.Queued, EnqueuedAt: now, ExpiresAt: now.Add(domain.QueueExpiryWindow)}, nil
|
||||||
@@ -269,6 +276,24 @@ func TestQueueAPIDelegatesAllMutationsAndRecoveryToBackend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQueueAPIUsesInjectedSessionBackend(t *testing.T) {
|
||||||
|
backend := &sessionBackendSpy{}
|
||||||
|
queue := &queueBackendSpy{}
|
||||||
|
service := &Service{SessionBackend: backend, QueueBackend: queue, Now: func() time.Time { return time.Unix(1000, 0).UTC() }}
|
||||||
|
server := httptest.NewServer(service.Handler())
|
||||||
|
defer server.Close()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, server.URL+"/v1/queue/ticket-1", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer durable-session:durable-token")
|
||||||
|
response, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK || backend.calls != 1 || queue.getCalls != 1 {
|
||||||
|
t.Fatalf("status=%d session_calls=%d queue_calls=%d", response.StatusCode, backend.calls, queue.getCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQueueRecoveryAPIIsAuthenticatedOwnerOnlyAndExpiresStaleTickets(t *testing.T) {
|
func TestQueueRecoveryAPIIsAuthenticatedOwnerOnlyAndExpiresStaleTickets(t *testing.T) {
|
||||||
now := time.Unix(1000, 0).UTC()
|
now := time.Unix(1000, 0).UTC()
|
||||||
sessions := domain.NewSessionStore()
|
sessions := domain.NewSessionStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user