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:
Josh Creek
2026-09-05 10:57:50 +01:00
parent d40344a2c0
commit f628ccfd35
12 changed files with 701 additions and 14 deletions
+23
View File
@@ -163,3 +163,26 @@ func ApplyIdentityBan(ctx context.Context, db *sql.DB, playerID, reason string,
return err
})
}
// IdentityUpsertSQL resolves a verified Steam ID to a durable player ID,
// creating the identity on first sign-in. The player ID is derived by the
// backend and never supplied by the client.
const IdentityUpsertSQL = `INSERT INTO identities (player_id, steam_id)
VALUES ($1, $2)
ON CONFLICT (steam_id) DO UPDATE SET steam_id = EXCLUDED.steam_id
RETURNING player_id`
// ResolveSteamIdentity returns the player ID for a verified Steam ID. The
// proposed ID is used only when this Steam ID has never signed in before; an
// existing identity keeps the player ID it already had, so a returning player
// keeps their ratings and penalties.
func ResolveSteamIdentity(ctx context.Context, db *sql.DB, steamID, proposedPlayerID string) (string, error) {
if db == nil || steamID == "" || proposedPlayerID == "" {
return "", domain.ErrTicketRejected
}
var playerID string
if err := db.QueryRowContext(ctx, IdentityUpsertSQL, proposedPlayerID, steamID).Scan(&playerID); err != nil {
return "", err
}
return playerID, nil
}