// Package sso implements the authorization code store for the SSO redirect flow. // // MCIAS acts as the SSO provider: downstream services (MCR, MCAT, Metacrypt) // redirect users to MCIAS for login, and MCIAS issues a short-lived, single-use // authorization code that the service exchanges for a JWT token. // // Security design: // - Authorization codes are 32 random bytes (256 bits), hex-encoded. // - Codes are single-use: consumed via sync.Map LoadAndDelete on first exchange. // - Codes expire after 60 seconds to limit the window for interception. // - A background goroutine evicts expired codes every 5 minutes. // - The code is bound to the client_id and redirect_uri presented at authorize // time; the token exchange endpoint must verify both match. package sso import ( "fmt" "sync" "time" "git.wntrmute.dev/mc/mcias/internal/crypto" ) const ( codeTTL = 60 * time.Second codeBytes = 32 // 256 bits of entropy cleanupPeriod = 5 * time.Minute ) // AuthCode is a pending authorization code awaiting exchange for a JWT. type AuthCode struct { //nolint:govet // fieldalignment: field order matches logical grouping ClientID string RedirectURI string State string AccountID int64 ExpiresAt time.Time } // pendingCodes stores issued authorization codes awaiting exchange. var pendingCodes sync.Map //nolint:gochecknoglobals func init() { go cleanupCodes() } func cleanupCodes() { ticker := time.NewTicker(cleanupPeriod) defer ticker.Stop() for range ticker.C { now := time.Now() pendingCodes.Range(func(key, value any) bool { ac, ok := value.(*AuthCode) if !ok || now.After(ac.ExpiresAt) { pendingCodes.Delete(key) } return true }) } } // Store creates and stores a new authorization code bound to the given // client_id, redirect_uri, state, and account. Returns the hex-encoded code. func Store(clientID, redirectURI, state string, accountID int64) (string, error) { raw, err := crypto.RandomBytes(codeBytes) if err != nil { return "", fmt.Errorf("sso: generate authorization code: %w", err) } code := fmt.Sprintf("%x", raw) pendingCodes.Store(code, &AuthCode{ ClientID: clientID, RedirectURI: redirectURI, State: state, AccountID: accountID, ExpiresAt: time.Now().Add(codeTTL), }) return code, nil } // Consume retrieves and deletes an authorization code. Returns the AuthCode // and true if the code was valid and not expired, or (nil, false) otherwise. // // Security: LoadAndDelete ensures single-use; the code cannot be replayed. func Consume(code string) (*AuthCode, bool) { v, ok := pendingCodes.LoadAndDelete(code) if !ok { return nil, false } ac, ok2 := v.(*AuthCode) if !ok2 || time.Now().After(ac.ExpiresAt) { return nil, false } return ac, true }