feat: model asynchronous Steam auth sessions

This commit is contained in:
Josh Creek
2026-08-31 22:21:31 +01:00
parent 1a43a342ea
commit 9fe6c57b5d
3 changed files with 151 additions and 1 deletions
+102
View File
@@ -28,6 +28,108 @@ type TicketVerifier struct {
consumed map[string]time.Time
}
type AuthAttemptState string
const (
AuthPending AuthAttemptState = "PENDING"
AuthAccepted AuthAttemptState = "ACCEPTED"
AuthRejected AuthAttemptState = "REJECTED"
AuthCancelled AuthAttemptState = "CANCELLED"
)
type AuthAttempt struct {
AttemptID string
TicketID string
State AuthAttemptState
Identity VerifiedIdentity
ExpiresAt time.Time
}
// AuthCoordinator models the asynchronous BeginAuthSession lifecycle. The
// external Steam adapter calls Complete only after Steam confirms the ticket;
// clients never supply the verified identity or transition state themselves.
type AuthCoordinator struct {
mu sync.Mutex
attempts map[string]AuthAttempt
}
var (
ErrAuthAttemptRejected = fmt.Errorf("auth attempt rejected")
ErrAuthAttemptPending = fmt.Errorf("auth attempt pending")
)
func NewAuthCoordinator() *AuthCoordinator {
return &AuthCoordinator{attempts: make(map[string]AuthAttempt)}
}
func (c *AuthCoordinator) Begin(attemptID string, ticket SteamTicket, now time.Time) error {
if c == nil || attemptID == "" || ticket.TicketID == "" || ticket.ExpiresAt.IsZero() || !now.Before(ticket.ExpiresAt) {
return ErrAuthAttemptRejected
}
c.mu.Lock()
defer c.mu.Unlock()
if _, exists := c.attempts[attemptID]; exists {
return ErrAuthAttemptRejected
}
c.attempts[attemptID] = AuthAttempt{AttemptID: attemptID, TicketID: ticket.TicketID, State: AuthPending, ExpiresAt: ticket.ExpiresAt}
return nil
}
func (c *AuthCoordinator) Complete(attemptID string, ticket SteamTicket, verifier *TicketVerifier, resolve func(string) (string, bool), now time.Time) (VerifiedIdentity, error) {
if c == nil || verifier == nil {
return VerifiedIdentity{}, ErrAuthAttemptRejected
}
c.mu.Lock()
attempt, ok := c.attempts[attemptID]
if !ok || attempt.State != AuthPending || attempt.TicketID != ticket.TicketID || !now.Before(attempt.ExpiresAt) {
c.mu.Unlock()
return VerifiedIdentity{}, ErrAuthAttemptRejected
}
identity, err := verifier.Verify(ticket, resolve, now)
if err != nil {
attempt.State = AuthRejected
c.attempts[attemptID] = attempt
c.mu.Unlock()
return VerifiedIdentity{}, ErrAuthAttemptRejected
}
attempt.State = AuthAccepted
attempt.Identity = identity
c.attempts[attemptID] = attempt
c.mu.Unlock()
return identity, nil
}
func (c *AuthCoordinator) Cancel(attemptID string) error {
if c == nil || attemptID == "" {
return ErrAuthAttemptRejected
}
c.mu.Lock()
defer c.mu.Unlock()
attempt, ok := c.attempts[attemptID]
if !ok || attempt.State != AuthPending {
return ErrAuthAttemptRejected
}
attempt.State = AuthCancelled
c.attempts[attemptID] = attempt
return nil
}
func (c *AuthCoordinator) Get(attemptID string) (AuthAttempt, error) {
if c == nil {
return AuthAttempt{}, ErrAuthAttemptRejected
}
c.mu.Lock()
defer c.mu.Unlock()
attempt, ok := c.attempts[attemptID]
if !ok {
return AuthAttempt{}, ErrAuthAttemptRejected
}
if attempt.State != AuthAccepted {
return attempt, ErrAuthAttemptPending
}
return attempt, nil
}
var (
ErrTicketRejected = fmt.Errorf("steam ticket rejected")
ErrSessionRejected = fmt.Errorf("session rejected")