package auth import ( "crypto/rand" "encoding/hex" "sync" "time" ) const defaultTokenTTL = time.Hour type tokenEntry struct { keyID string expiresAt time.Time } // TokenStore issues and validates short-lived opaque access tokens for OAuth // client credentials flow. type TokenStore struct { mu sync.RWMutex tokens map[string]tokenEntry ttl time.Duration } func NewTokenStore(ttl time.Duration) *TokenStore { if ttl <= 0 { ttl = defaultTokenTTL } s := &TokenStore{ tokens: make(map[string]tokenEntry), ttl: ttl, } go s.sweepLoop() return s } // Issue generates a new token for the given keyID and returns the token and its TTL. func (s *TokenStore) Issue(keyID string) (string, time.Duration, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", 0, err } token := hex.EncodeToString(b) s.mu.Lock() s.tokens[token] = tokenEntry{keyID: keyID, expiresAt: time.Now().Add(s.ttl)} s.mu.Unlock() return token, s.ttl, nil } // Lookup validates a token and returns the associated keyID. func (s *TokenStore) Lookup(token string) (string, bool) { s.mu.RLock() entry, ok := s.tokens[token] s.mu.RUnlock() if !ok || time.Now().After(entry.expiresAt) { return "", false } return entry.keyID, true } func (s *TokenStore) sweepLoop() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { now := time.Now() s.mu.Lock() for token, entry := range s.tokens { if now.After(entry.expiresAt) { delete(s.tokens, token) } } s.mu.Unlock() } }