package sso import ( "fmt" "sync" "time" "git.wntrmute.dev/mc/mcias/internal/crypto" ) const ( sessionTTL = 5 * time.Minute sessionBytes = 16 // 128 bits of entropy for the nonce ) // Session holds the SSO parameters between /sso/authorize and login completion. // The nonce is embedded as a hidden form field in the login page and carried // through the multi-step login flow (password → TOTP, or WebAuthn). type Session struct { //nolint:govet // fieldalignment: field order matches logical grouping ClientID string RedirectURI string State string ExpiresAt time.Time } // pendingSessions stores SSO sessions created at /sso/authorize. var pendingSessions sync.Map //nolint:gochecknoglobals func init() { go cleanupSessions() } func cleanupSessions() { ticker := time.NewTicker(cleanupPeriod) defer ticker.Stop() for range ticker.C { now := time.Now() pendingSessions.Range(func(key, value any) bool { s, ok := value.(*Session) if !ok || now.After(s.ExpiresAt) { pendingSessions.Delete(key) } return true }) } } // StoreSession creates and stores a new SSO session, returning the hex-encoded // nonce that should be embedded in the login form. func StoreSession(clientID, redirectURI, state string) (string, error) { raw, err := crypto.RandomBytes(sessionBytes) if err != nil { return "", fmt.Errorf("sso: generate session nonce: %w", err) } nonce := fmt.Sprintf("%x", raw) pendingSessions.Store(nonce, &Session{ ClientID: clientID, RedirectURI: redirectURI, State: state, ExpiresAt: time.Now().Add(sessionTTL), }) return nonce, nil } // ConsumeSession retrieves and deletes an SSO session by nonce. // Returns the Session and true if valid, or (nil, false) if unknown or expired. func ConsumeSession(nonce string) (*Session, bool) { v, ok := pendingSessions.LoadAndDelete(nonce) if !ok { return nil, false } s, ok2 := v.(*Session) if !ok2 || time.Now().After(s.ExpiresAt) { return nil, false } return s, true } // GetSession retrieves an SSO session without consuming it (for read-only checks // during multi-step login). Returns nil if unknown or expired. func GetSession(nonce string) *Session { v, ok := pendingSessions.Load(nonce) if !ok { return nil } s, ok2 := v.(*Session) if !ok2 || time.Now().After(s.ExpiresAt) { return nil } return s }