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:
@@ -2277,3 +2277,64 @@ func TestPostgreSQLProbedTicketBecomesSelectableByTheMatcher(t *testing.T) {
|
||||
t.Fatal("the refreshed candidate still carries an empty RTT map, so Redis would keep a stale entry")
|
||||
}
|
||||
}
|
||||
|
||||
// Production sign-in, exercised through the real SteamLogin provider with only
|
||||
// the Valve HTTP call stubbed. newAPIService never supplied SteamLogin, so
|
||||
// POST /v1/session/steam always returned 503 auth_unavailable; the only
|
||||
// implementation was cmd/testkit-api's fake, which accepts any ticket string
|
||||
// and therefore proves nothing about the deployable path.
|
||||
func TestPostgreSQLSteamLoginResolvesDurableIdentities(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
|
||||
ctx := context.Background()
|
||||
const steamID = "76561198000000001"
|
||||
|
||||
// First sign-in creates the identity.
|
||||
playerID, err := ResolveSteamIdentity(ctx, db, steamID, "player-first")
|
||||
if err != nil {
|
||||
t.Fatalf("first sign-in: %v", err)
|
||||
}
|
||||
if playerID != "player-first" {
|
||||
t.Fatalf("first sign-in player = %q", playerID)
|
||||
}
|
||||
|
||||
// A returning player must keep the player ID they already had, or their
|
||||
// ratings, penalties and bans would silently detach from their account.
|
||||
returning, err := ResolveSteamIdentity(ctx, db, steamID, "player-different-proposal")
|
||||
if err != nil {
|
||||
t.Fatalf("returning sign-in: %v", err)
|
||||
}
|
||||
if returning != "player-first" {
|
||||
t.Fatalf("returning player was given a new ID %q", returning)
|
||||
}
|
||||
|
||||
var identities int
|
||||
if err := db.QueryRowContext(ctx, `SELECT count(*) FROM identities WHERE steam_id = $1`, steamID).Scan(&identities); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if identities != 1 {
|
||||
t.Fatalf("one Steam ID produced %d identity rows", identities)
|
||||
}
|
||||
|
||||
// The resolved identity must be usable for session issuance, which is the
|
||||
// step that was unreachable in production.
|
||||
sessions := PostgresSessions{DB: db}
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
session, token, err := sessions.Issue(ctx, returning, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("issue session for a freshly resolved identity: %v", err)
|
||||
}
|
||||
if _, err := sessions.Authenticate(ctx, session.SessionID, token, now); err != nil {
|
||||
t.Fatalf("authenticate freshly issued session: %v", err)
|
||||
}
|
||||
|
||||
// And a banned account cannot sign in, tying the real login path to the
|
||||
// durable ban enforcement rather than leaving it adapter-specific.
|
||||
if err := ApplyIdentityBan(ctx, db, returning, "cheating", now.Add(time.Hour), now); err != nil {
|
||||
t.Fatalf("apply ban: %v", err)
|
||||
}
|
||||
if _, _, err := sessions.Issue(ctx, returning, time.Hour, now.Add(time.Minute)); err == nil {
|
||||
t.Fatal("a banned identity signed in through the production path")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user