Files

56 lines
1.5 KiB
Go

package pds
import (
"sync"
"time"
)
// jtiReplayCache tracks JWT IDs of recently-seen appview tokens so the same
// token can't be replayed within its TTL. Entries auto-expire at the token's
// claimed expiration time (no background sweeper needed — eviction happens
// lazily on Seen()).
type jtiReplayCache struct {
mu sync.Mutex
entries map[string]time.Time
clock func() time.Time
}
func newJTIReplayCache() *jtiReplayCache {
return &jtiReplayCache{entries: make(map[string]time.Time), clock: time.Now}
}
// Seen records `jti` with the given expiration and returns true if the jti was
// already present (i.e., this is a replay). It opportunistically evicts any
// already-expired entries it encounters.
func (c *jtiReplayCache) Seen(jti string, exp time.Time) bool {
c.mu.Lock()
defer c.mu.Unlock()
now := c.clock()
// lazy eviction: drop the entry we're about to look at if it's expired
if prev, ok := c.entries[jti]; ok {
if !prev.After(now) {
delete(c.entries, jti)
} else {
return true
}
}
// opportunistic sweep — keeps the map bounded under steady-state load
// without needing a goroutine. O(n) but n is small in practice.
if len(c.entries) > 0 && len(c.entries)%64 == 0 {
for k, e := range c.entries {
if !e.After(now) {
delete(c.entries, k)
}
}
}
c.entries[jti] = exp
return false
}
// size returns the number of tracked entries (test-only helper).
func (c *jtiReplayCache) size() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.entries)
}