add RevocationStore interface and in-memory implementation

This commit is contained in:
juancwu 2026-04-29 01:40:35 +00:00
commit 5288df0b9e
2 changed files with 231 additions and 0 deletions

86
revocation.go Normal file
View file

@ -0,0 +1,86 @@
package ficha
import (
"context"
"sync"
"time"
)
// RevocationStore is the interface ficha uses to track revoked tokens.
// Implementations are responsible for storage; ficha provides only the
// in-memory reference implementation below.
//
// Implementations must be safe for concurrent use.
type RevocationStore interface {
// IsRevoked reports whether tokenID has been revoked.
IsRevoked(ctx context.Context, tokenID string) (bool, error)
// Revoke marks tokenID as revoked. The until parameter is the
// token's natural expiry — implementations may discard the entry
// after that time, since expired tokens fail validation anyway.
Revoke(ctx context.Context, tokenID string, until time.Time) error
}
// MemoryRevocationStore is an in-memory RevocationStore suitable for
// tests, single-process deployments, or as a reference implementation.
// Not suitable for production multi-server use — entries are not shared.
type MemoryRevocationStore struct {
mu sync.RWMutex
revoked map[string]time.Time
now func() time.Time
}
// NewMemoryRevocationStore returns an empty in-memory store.
func NewMemoryRevocationStore() *MemoryRevocationStore {
return &MemoryRevocationStore{
revoked: make(map[string]time.Time),
now: time.Now,
}
}
func (m *MemoryRevocationStore) IsRevoked(_ context.Context, tokenID string) (bool, error) {
m.mu.RLock()
defer m.mu.RUnlock()
until, ok := m.revoked[tokenID]
if !ok {
return false, nil
}
if !m.now().Before(until) {
// Past expiry — would fail validation regardless. Treat as not revoked.
return false, nil
}
return true, nil
}
func (m *MemoryRevocationStore) Revoke(_ context.Context, tokenID string, until time.Time) error {
m.mu.Lock()
defer m.mu.Unlock()
m.revoked[tokenID] = until
return nil
}
// Cleanup removes expired entries. Call periodically to bound memory use.
// Returns the number of entries removed.
func (m *MemoryRevocationStore) Cleanup() int {
m.mu.Lock()
defer m.mu.Unlock()
now := m.now()
removed := 0
for id, until := range m.revoked {
if !now.Before(until) {
delete(m.revoked, id)
removed++
}
}
return removed
}
// Len returns the current number of tracked entries (including any not
// yet cleaned up). Mainly useful for tests and metrics.
func (m *MemoryRevocationStore) Len() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.revoked)
}