mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-14 09:52:07 +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:
@@ -0,0 +1,53 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/steam"
|
||||
"github.com/cosmic-clash/cosmic-clash/server/store"
|
||||
)
|
||||
|
||||
// SteamTicketVerifier is the boundary to Valve. Keeping it an interface means
|
||||
// the production login path can be exercised end to end with the external call
|
||||
// stubbed, instead of only through a fake login provider that skips the whole
|
||||
// flow.
|
||||
type SteamTicketVerifier interface {
|
||||
Verify(ctx context.Context, ticket string) (steam.Identity, error)
|
||||
}
|
||||
|
||||
// SteamLogin is the production SteamLoginProvider: verify the ticket with
|
||||
// Valve, then resolve the verified Steam ID to a durable player ID.
|
||||
type SteamLogin struct {
|
||||
DB *sql.DB
|
||||
Verifier SteamTicketVerifier
|
||||
}
|
||||
|
||||
// PlayerIDForSteamID derives the durable player ID for a Steam ID on first
|
||||
// sign-in. It is a hash rather than the Steam ID itself so player IDs, which
|
||||
// appear in rosters and logs, do not restate the platform identifier.
|
||||
func PlayerIDForSteamID(steamID string) string {
|
||||
digest := sha256.Sum256([]byte("cosmic-clash/player/" + steamID))
|
||||
return "player-" + hex.EncodeToString(digest[:12])
|
||||
}
|
||||
|
||||
func (s SteamLogin) Authenticate(ctx context.Context, ticket string, _ time.Time) (domain.VerifiedIdentity, error) {
|
||||
if s.DB == nil || s.Verifier == nil {
|
||||
return domain.VerifiedIdentity{}, domain.ErrTicketRejected
|
||||
}
|
||||
identity, err := s.Verifier.Verify(ctx, ticket)
|
||||
if err != nil {
|
||||
return domain.VerifiedIdentity{}, err
|
||||
}
|
||||
// A returning player keeps the player ID they already had, so ratings,
|
||||
// penalties and bans follow the account rather than the session.
|
||||
playerID, err := store.ResolveSteamIdentity(ctx, s.DB, identity.SteamID, PlayerIDForSteamID(identity.SteamID))
|
||||
if err != nil {
|
||||
return domain.VerifiedIdentity{}, err
|
||||
}
|
||||
return domain.VerifiedIdentity{PlayerID: playerID, SteamID: identity.SteamID}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user