mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat(auth): wire production Steam sign-in and the client login flow
newAPIService never supplied SteamLogin, so POST /v1/session/steam always returned 503 auth_unavailable in production. The only implementation was cmd/testkit-api's fake, which derives an identity from the ticket string itself and accepts anything -- so the passing integration path was neither deployable nor secure. On the client side the game started with an empty token and a loopback base URL, and no production code called configure() or login_steam(); the menu entered matchmaking directly, so every request failed ERR_UNAUTHORIZED before reaching the network. Add a real ISteamUserAuth/AuthenticateUserTicket adapter behind an interface, so the production login path is testable with only the Valve call stubbed. It rejects family-shared copies (the account playing does not own the app) and, by default, VAC- or publisher-banned accounts, and refuses malformed tickets locally rather than forwarding them. Crucially it separates our faults from the player's: a Valve outage or a revoked publisher key returns 503, not 401. Answering 401 would tell a legitimate player their login failed and send them to fix an account that is fine while the real fault went unnoticed. A banned identity now returns 403 rather than a misleading 503. Sign-in is configuration-gated on the publisher key and App ID: without them the endpoint keeps returning 503, since silently accepting an unverified ticket would be worse than refusing to authenticate. A returning player keeps the player ID they already had, so ratings, penalties and bans follow the account rather than the session. Client side: acquire a web-API ticket through GodotSteam's async signal -- requesting one returns a handle, not a ticket -- using the existing dynamic-call pattern so stock Godot still parses the project. The endpoint is configurable for release builds, and matchmaking completes sign-in before it will queue. Verified against real PostgreSQL; 232 Godot tests pass.
This commit is contained in:
+33
-14
@@ -20,12 +20,14 @@ import (
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/observability"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/steam"
|
||||
)
|
||||
|
||||
const maxBodyBytes = 8 << 10
|
||||
|
||||
type CandidateProvider func(playerID, ticketID string) (domain.Candidate, error)
|
||||
type CandidateProviderV2 func(playerID, ticketID string, spec domain.QueueSpec) (domain.Candidate, error)
|
||||
|
||||
// ProbeProvider validates a probe answer against the nonce the backend issued
|
||||
// and returns evidence whose ServerRTT is derived from backend timestamps
|
||||
// only. It takes a context because the issued nonce is durable: any replica
|
||||
@@ -116,25 +118,25 @@ type RosterProvider func(context.Context, domain.WorkloadBinding, time.Time) ([]
|
||||
type ReadinessCheck func(context.Context) error
|
||||
|
||||
type Service struct {
|
||||
Sessions *domain.SessionStore
|
||||
SessionBackend SessionBackend
|
||||
SessionIssuer SessionIssuer
|
||||
SteamLogin SteamLoginProvider
|
||||
Queue *domain.Queue
|
||||
Candidate CandidateProvider
|
||||
CandidateV2 CandidateProviderV2
|
||||
QueueBackend QueueBackend
|
||||
CandidateIndex CandidateIndex
|
||||
Sessions *domain.SessionStore
|
||||
SessionBackend SessionBackend
|
||||
SessionIssuer SessionIssuer
|
||||
SteamLogin SteamLoginProvider
|
||||
Queue *domain.Queue
|
||||
Candidate CandidateProvider
|
||||
CandidateV2 CandidateProviderV2
|
||||
QueueBackend QueueBackend
|
||||
CandidateIndex CandidateIndex
|
||||
// EventFanout, when set, publishes outbox-sourced events through a shared
|
||||
// transport instead of only this replica's in-memory hub. Without it a
|
||||
// client connected to a replica other than the one that drained the outbox
|
||||
// row never receives the event.
|
||||
EventFanout func(ControlPlaneEvent) error
|
||||
Probe ProbeProvider
|
||||
ProbeChallenger ProbeChallengeIssuer
|
||||
EventFanout func(ControlPlaneEvent) error
|
||||
Probe ProbeProvider
|
||||
ProbeChallenger ProbeChallengeIssuer
|
||||
// CandidateRefresh re-reads a player's durable queue candidate so the
|
||||
// transient index can be corrected after its RTT changes.
|
||||
CandidateRefresh func(context.Context, string, time.Time) (domain.Candidate, bool, error)
|
||||
CandidateRefresh func(context.Context, string, time.Time) (domain.Candidate, bool, error)
|
||||
ProbeRecorder ProbeRecorder
|
||||
WorkloadVerify WorkloadVerifier
|
||||
ResultSubmitter ResultSubmitter
|
||||
@@ -355,7 +357,18 @@ func (s *Service) steamSession(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
now := s.now()
|
||||
identity, err := s.SteamLogin.Authenticate(r.Context(), input.WebAPITicket, now)
|
||||
if err != nil || identity.PlayerID == "" || identity.SteamID == "" {
|
||||
if err != nil {
|
||||
// A Valve outage or a bad publisher key is our problem, not the
|
||||
// player's; answering 401 would tell a legitimate player their login
|
||||
// failed and send them off to fix an account that is fine.
|
||||
if errors.Is(err, steam.ErrUnavailable) {
|
||||
writeError(w, http.StatusServiceUnavailable, "auth_unavailable")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
if identity.PlayerID == "" || identity.SteamID == "" {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
@@ -370,6 +383,12 @@ func (s *Service) steamSession(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// Session issuance refuses an actively banned identity. That is a
|
||||
// decision about this account, not an outage.
|
||||
if errors.Is(err, domain.ErrSessionRejected) {
|
||||
writeError(w, http.StatusForbidden, "identity_banned")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusServiceUnavailable, "auth_unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user