package auth import ( "crypto/rand" "encoding/hex" "sync" "time" ) const authCodeTTL = 10 * time.Minute // AuthCode holds a pending authorization code and its associated PKCE data. type AuthCode struct { ClientID string RedirectURI string Scope string CodeChallenge string CodeChallengeMethod string KeyID string ExpiresAt time.Time } // AuthCodeStore issues single-use authorization codes for the OAuth 2.0 // Authorization Code flow. type AuthCodeStore struct { mu sync.Mutex codes map[string]AuthCode } func NewAuthCodeStore() *AuthCodeStore { s := &AuthCodeStore{codes: make(map[string]AuthCode)} go s.sweepLoop() return s } // Issue stores the entry and returns the raw authorization code. func (s *AuthCodeStore) Issue(entry AuthCode) (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } raw := hex.EncodeToString(b) entry.ExpiresAt = time.Now().Add(authCodeTTL) s.mu.Lock() s.codes[raw] = entry s.mu.Unlock() return raw, nil } // Consume validates and removes the code, returning the associated entry. func (s *AuthCodeStore) Consume(code string) (AuthCode, bool) { s.mu.Lock() defer s.mu.Unlock() entry, ok := s.codes[code] if !ok || time.Now().After(entry.ExpiresAt) { delete(s.codes, code) return AuthCode{}, false } delete(s.codes, code) return entry, true } func (s *AuthCodeStore) sweepLoop() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { now := time.Now() s.mu.Lock() for code, entry := range s.codes { if now.After(entry.ExpiresAt) { delete(s.codes, code) } } s.mu.Unlock() } }