A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
fork

Configure Feed

Select the types of activity you want to include in your feed.

at refactor 142 lines 4.1 kB view raw
1// Package token provides service token caching and management for AppView. 2// Service tokens are JWTs issued by a user's PDS to authorize AppView to 3// act on their behalf when communicating with hold services. Tokens are 4// cached with automatic expiry parsing and 10-second safety margins. 5package auth 6 7import ( 8 "log/slog" 9 "sync" 10 "time" 11) 12 13// serviceTokenEntry represents a cached service token 14type serviceTokenEntry struct { 15 token string 16 expiresAt time.Time 17 err error 18 once sync.Once 19} 20 21// Global cache for service tokens (DID:HoldDID -> token) 22// Service tokens are JWTs issued by a user's PDS to authorize AppView to act on their behalf 23// when communicating with hold services. These tokens are scoped to specific holds and have 24// limited lifetime (typically 60s, can request up to 5min). 25var ( 26 globalServiceTokens = make(map[string]*serviceTokenEntry) 27 globalServiceTokensMu sync.RWMutex 28) 29 30// GetServiceToken retrieves a cached service token for the given DID and hold DID 31// Returns empty string if no valid cached token exists 32func GetServiceToken(did, holdDID string) (token string, expiresAt time.Time) { 33 cacheKey := did + ":" + holdDID 34 35 globalServiceTokensMu.RLock() 36 entry, exists := globalServiceTokens[cacheKey] 37 globalServiceTokensMu.RUnlock() 38 39 if !exists { 40 return "", time.Time{} 41 } 42 43 // Check if token is still valid 44 if time.Now().After(entry.expiresAt) { 45 // Token expired, remove from cache 46 globalServiceTokensMu.Lock() 47 delete(globalServiceTokens, cacheKey) 48 globalServiceTokensMu.Unlock() 49 return "", time.Time{} 50 } 51 52 return entry.token, entry.expiresAt 53} 54 55// SetServiceToken stores a service token in the cache 56// Automatically parses the JWT to extract the expiry time 57// Applies a 10-second safety margin (cache expires 10s before actual JWT expiry) 58func SetServiceToken(did, holdDID, token string) error { 59 cacheKey := did + ":" + holdDID 60 61 // Parse JWT to extract expiry (don't verify signature - we trust the PDS) 62 expiry, err := ParseJWTExpiry(token) 63 if err != nil { 64 // If parsing fails, use default 50s TTL (conservative fallback) 65 slog.Warn("Failed to parse JWT expiry, using default 50s", "error", err, "cacheKey", cacheKey) 66 expiry = time.Now().Add(50 * time.Second) 67 } else { 68 // Apply 10s safety margin to avoid using nearly-expired tokens 69 expiry = expiry.Add(-10 * time.Second) 70 } 71 72 globalServiceTokensMu.Lock() 73 globalServiceTokens[cacheKey] = &serviceTokenEntry{ 74 token: token, 75 expiresAt: expiry, 76 } 77 globalServiceTokensMu.Unlock() 78 79 slog.Debug("Cached service token", 80 "cacheKey", cacheKey, 81 "expiresIn", time.Until(expiry).Round(time.Second)) 82 83 return nil 84} 85 86// InvalidateServiceToken removes a service token from the cache 87// Used when we detect that a token is invalid or the user's session has expired 88func InvalidateServiceToken(did, holdDID string) { 89 cacheKey := did + ":" + holdDID 90 91 globalServiceTokensMu.Lock() 92 delete(globalServiceTokens, cacheKey) 93 globalServiceTokensMu.Unlock() 94 95 slog.Debug("Invalidated service token", "cacheKey", cacheKey) 96} 97 98// GetCacheStats returns statistics about the service token cache for debugging 99func GetCacheStats() map[string]any { 100 globalServiceTokensMu.RLock() 101 defer globalServiceTokensMu.RUnlock() 102 103 validCount := 0 104 expiredCount := 0 105 now := time.Now() 106 107 for _, entry := range globalServiceTokens { 108 if now.Before(entry.expiresAt) { 109 validCount++ 110 } else { 111 expiredCount++ 112 } 113 } 114 115 return map[string]any{ 116 "total_entries": len(globalServiceTokens), 117 "valid_tokens": validCount, 118 "expired_tokens": expiredCount, 119 } 120} 121 122// CleanExpiredTokens removes expired tokens from the cache 123// Can be called periodically to prevent unbounded growth (though expired tokens 124// are also removed lazily on access) 125func CleanExpiredTokens() { 126 globalServiceTokensMu.Lock() 127 defer globalServiceTokensMu.Unlock() 128 129 now := time.Now() 130 removed := 0 131 132 for key, entry := range globalServiceTokens { 133 if now.After(entry.expiresAt) { 134 delete(globalServiceTokens, key) 135 removed++ 136 } 137 } 138 139 if removed > 0 { 140 slog.Debug("Cleaned expired service tokens", "count", removed) 141 } 142}