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

Configure Feed

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

at 97d1b3cdd50e4727e5db3c498f4e8bb73851fd39 169 lines 4.9 kB view raw
1package token 2 3import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10) 11 12// serviceTokenEntry represents a cached service token 13type serviceTokenEntry struct { 14 token string 15 expiresAt time.Time 16} 17 18// Global cache for service tokens (DID:HoldDID -> token) 19// Service tokens are JWTs issued by a user's PDS to authorize AppView to act on their behalf 20// when communicating with hold services. These tokens are scoped to specific holds and have 21// limited lifetime (typically 60s, can request up to 5min). 22var ( 23 globalServiceTokens = make(map[string]*serviceTokenEntry) 24 globalServiceTokensMu sync.RWMutex 25) 26 27// GetServiceToken retrieves a cached service token for the given DID and hold DID 28// Returns empty string if no valid cached token exists 29func GetServiceToken(did, holdDID string) (token string, expiresAt time.Time) { 30 cacheKey := did + ":" + holdDID 31 32 globalServiceTokensMu.RLock() 33 entry, exists := globalServiceTokens[cacheKey] 34 globalServiceTokensMu.RUnlock() 35 36 if !exists { 37 return "", time.Time{} 38 } 39 40 // Check if token is still valid 41 if time.Now().After(entry.expiresAt) { 42 // Token expired, remove from cache 43 globalServiceTokensMu.Lock() 44 delete(globalServiceTokens, cacheKey) 45 globalServiceTokensMu.Unlock() 46 return "", time.Time{} 47 } 48 49 return entry.token, entry.expiresAt 50} 51 52// SetServiceToken stores a service token in the cache 53// Automatically parses the JWT to extract the expiry time 54// Applies a 10-second safety margin (cache expires 10s before actual JWT expiry) 55func SetServiceToken(did, holdDID, token string) error { 56 cacheKey := did + ":" + holdDID 57 58 // Parse JWT to extract expiry (don't verify signature - we trust the PDS) 59 expiry, err := parseJWTExpiry(token) 60 if err != nil { 61 // If parsing fails, use default 50s TTL (conservative fallback) 62 fmt.Printf("WARN [token/cache]: Failed to parse JWT expiry, using default 50s: %v\n", err) 63 expiry = time.Now().Add(50 * time.Second) 64 } else { 65 // Apply 10s safety margin to avoid using nearly-expired tokens 66 expiry = expiry.Add(-10 * time.Second) 67 } 68 69 globalServiceTokensMu.Lock() 70 globalServiceTokens[cacheKey] = &serviceTokenEntry{ 71 token: token, 72 expiresAt: expiry, 73 } 74 globalServiceTokensMu.Unlock() 75 76 fmt.Printf("DEBUG [token/cache]: Cached service token for %s (expires in %v)\n", 77 cacheKey, time.Until(expiry).Round(time.Second)) 78 79 return nil 80} 81 82// parseJWTExpiry extracts the expiry time from a JWT without verifying the signature 83// We trust tokens from the user's PDS, so signature verification isn't needed here 84// Manually decodes the JWT payload to avoid algorithm compatibility issues 85func parseJWTExpiry(tokenString string) (time.Time, error) { 86 // JWT format: header.payload.signature 87 parts := strings.Split(tokenString, ".") 88 if len(parts) != 3 { 89 return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts)) 90 } 91 92 // Decode the payload (second part) 93 payload, err := base64.RawURLEncoding.DecodeString(parts[1]) 94 if err != nil { 95 return time.Time{}, fmt.Errorf("failed to decode JWT payload: %w", err) 96 } 97 98 // Parse the JSON payload 99 var claims struct { 100 Exp int64 `json:"exp"` 101 } 102 if err := json.Unmarshal(payload, &claims); err != nil { 103 return time.Time{}, fmt.Errorf("failed to parse JWT claims: %w", err) 104 } 105 106 if claims.Exp == 0 { 107 return time.Time{}, fmt.Errorf("JWT missing exp claim") 108 } 109 110 return time.Unix(claims.Exp, 0), nil 111} 112 113// InvalidateServiceToken removes a service token from the cache 114// Used when we detect that a token is invalid or the user's session has expired 115func InvalidateServiceToken(did, holdDID string) { 116 cacheKey := did + ":" + holdDID 117 118 globalServiceTokensMu.Lock() 119 delete(globalServiceTokens, cacheKey) 120 globalServiceTokensMu.Unlock() 121 122 fmt.Printf("DEBUG [token/cache]: Invalidated service token for %s\n", cacheKey) 123} 124 125// GetCacheStats returns statistics about the service token cache for debugging 126func GetCacheStats() map[string]interface{} { 127 globalServiceTokensMu.RLock() 128 defer globalServiceTokensMu.RUnlock() 129 130 validCount := 0 131 expiredCount := 0 132 now := time.Now() 133 134 for _, entry := range globalServiceTokens { 135 if now.Before(entry.expiresAt) { 136 validCount++ 137 } else { 138 expiredCount++ 139 } 140 } 141 142 return map[string]interface{}{ 143 "total_entries": len(globalServiceTokens), 144 "valid_tokens": validCount, 145 "expired_tokens": expiredCount, 146 } 147} 148 149// CleanExpiredTokens removes expired tokens from the cache 150// Can be called periodically to prevent unbounded growth (though expired tokens 151// are also removed lazily on access) 152func CleanExpiredTokens() { 153 globalServiceTokensMu.Lock() 154 defer globalServiceTokensMu.Unlock() 155 156 now := time.Now() 157 removed := 0 158 159 for key, entry := range globalServiceTokens { 160 if now.After(entry.expiresAt) { 161 delete(globalServiceTokens, key) 162 removed++ 163 } 164 } 165 166 if removed > 0 { 167 fmt.Printf("DEBUG [token/cache]: Cleaned %d expired service tokens\n", removed) 168 } 169}