Phase 2 — internal/storage/: Content-addressed blob storage with atomic writes via rename. BlobWriter stages data in uploads dir with running SHA-256 hash, commits by verifying digest then renaming to layers/sha256/<prefix>/<hex>. Reader provides Open, Stat, Delete, Exists with digest validation. Phase 3 — internal/auth/ + internal/server/: MCIAS client with Login and ValidateToken, 30s SHA-256-keyed cache with lazy eviction and injectable clock for testing. TLS 1.3 minimum with optional custom CA cert. Chi router with RequireAuth middleware (Bearer token extraction, WWW-Authenticate header, OCI error format), token endpoint (Basic auth → bearer exchange via MCIAS), and /v2/ version check handler. 52 tests passing (14 storage + 9 auth + 9 server + 20 existing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
66 lines
1.6 KiB
Go
66 lines
1.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// cacheEntry holds a cached Claims value and its expiration time.
|
|
type cacheEntry struct {
|
|
claims *Claims
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// validationCache provides a concurrency-safe, TTL-based cache for token
|
|
// validation results. Tokens are keyed by their SHA-256 hex digest.
|
|
type validationCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]cacheEntry
|
|
ttl time.Duration
|
|
now func() time.Time // injectable clock for testing
|
|
}
|
|
|
|
// newCache creates a validationCache with the given TTL.
|
|
func newCache(ttl time.Duration) *validationCache {
|
|
return &validationCache{
|
|
entries: make(map[string]cacheEntry),
|
|
ttl: ttl,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
// get returns cached claims for the given token hash, or false if the
|
|
// entry is missing or expired. Expired entries are lazily evicted.
|
|
func (c *validationCache) get(tokenHash string) (*Claims, bool) {
|
|
c.mu.RLock()
|
|
entry, ok := c.entries[tokenHash]
|
|
c.mu.RUnlock()
|
|
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
if c.now().After(entry.expiresAt) {
|
|
// Lazy evict the expired entry.
|
|
c.mu.Lock()
|
|
// Re-check under write lock in case another goroutine already evicted.
|
|
if e, exists := c.entries[tokenHash]; exists && c.now().After(e.expiresAt) {
|
|
delete(c.entries, tokenHash)
|
|
}
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
|
|
return entry.claims, true
|
|
}
|
|
|
|
// put stores claims in the cache with an expiration of now + TTL.
|
|
func (c *validationCache) put(tokenHash string, claims *Claims) {
|
|
c.mu.Lock()
|
|
c.entries[tokenHash] = cacheEntry{
|
|
claims: claims,
|
|
expiresAt: c.now().Add(c.ttl),
|
|
}
|
|
c.mu.Unlock()
|
|
}
|