Move Go reference implementation to refimpl/go-lang/ and add new Python reference implementation in refimpl/python/. Update build.sh with renamed draft and simplified tool paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
70 lines
1.5 KiB
Go
70 lines
1.5 KiB
Go
package ect
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// JTICache provides replay protection by remembering seen JTIs. Safe for concurrent use.
|
|
// Use as VerifyOptions.JTISeen: cache.Seen(jti) and call cache.Add(jti) after successful verify.
|
|
type JTICache interface {
|
|
// Seen returns true if jti was already added (replay).
|
|
Seen(jti string) bool
|
|
// Add records jti. Call after successful verification before accepting the token.
|
|
Add(jti string)
|
|
}
|
|
|
|
type jtiEntry struct {
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// NewJTICache returns an in-memory JTI cache with optional max size and TTL.
|
|
// maxSize 0 means unbounded; entries are evicted after ttl.
|
|
func NewJTICache(maxSize int, ttl time.Duration) JTICache {
|
|
return &jtiCache{
|
|
byJti: make(map[string]jtiEntry),
|
|
maxSize: maxSize,
|
|
ttl: ttl,
|
|
}
|
|
}
|
|
|
|
type jtiCache struct {
|
|
mu sync.RWMutex
|
|
byJti map[string]jtiEntry
|
|
maxSize int
|
|
ttl time.Duration
|
|
}
|
|
|
|
func (c *jtiCache) Seen(jti string) bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
e, ok := c.byJti[jti]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if time.Now().After(e.expiresAt) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *jtiCache) Add(jti string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
now := time.Now()
|
|
// Evict expired
|
|
for k, v := range c.byJti {
|
|
if now.After(v.expiresAt) {
|
|
delete(c.byJti, k)
|
|
}
|
|
}
|
|
if c.maxSize > 0 && len(c.byJti) >= c.maxSize && c.byJti[jti].expiresAt.IsZero() {
|
|
// Evict one oldest (simple: remove first we see)
|
|
for k := range c.byJti {
|
|
delete(c.byJti, k)
|
|
break
|
|
}
|
|
}
|
|
c.byJti[jti] = jtiEntry{expiresAt: now.Add(c.ttl)}
|
|
}
|