A community based topic aggregation platform built on atproto
1package communities
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "time"
9)
10
11// parseJWTExpiration extracts the expiration time from a JWT access token
12// This function does NOT verify the signature - it only parses the exp claim
13// atproto access tokens use standard JWT format with 'exp' claim (Unix timestamp)
14func parseJWTExpiration(token string) (time.Time, error) {
15 // Remove "Bearer " prefix if present
16 token = strings.TrimPrefix(token, "Bearer ")
17 token = strings.TrimSpace(token)
18
19 // JWT format: header.payload.signature
20 parts := strings.Split(token, ".")
21 if len(parts) != 3 {
22 return time.Time{}, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
23 }
24
25 // Decode payload (second part) - use RawURLEncoding (no padding)
26 payload, err := base64.RawURLEncoding.DecodeString(parts[1])
27 if err != nil {
28 return time.Time{}, fmt.Errorf("failed to decode JWT payload: %w", err)
29 }
30
31 // Extract exp claim (Unix timestamp)
32 var claims struct {
33 Exp int64 `json:"exp"` // Expiration time (seconds since Unix epoch)
34 }
35 if err := json.Unmarshal(payload, &claims); err != nil {
36 return time.Time{}, fmt.Errorf("failed to parse JWT claims: %w", err)
37 }
38
39 if claims.Exp == 0 {
40 return time.Time{}, fmt.Errorf("JWT missing 'exp' claim")
41 }
42
43 // Convert Unix timestamp to time.Time
44 return time.Unix(claims.Exp, 0), nil
45}
46
47// NeedsRefresh checks if an access token should be refreshed
48// Returns true if the token expires within the next 5 minutes (or is already expired)
49// Uses a 5-minute buffer to ensure we refresh before actual expiration
50func NeedsRefresh(accessToken string) (bool, error) {
51 if accessToken == "" {
52 return false, fmt.Errorf("access token is empty")
53 }
54
55 expiration, err := parseJWTExpiration(accessToken)
56 if err != nil {
57 return false, fmt.Errorf("failed to parse token expiration: %w", err)
58 }
59
60 // Refresh if token expires within 5 minutes
61 // This prevents service interruptions from expired tokens
62 bufferTime := 5 * time.Minute
63 expiresWithinBuffer := time.Now().Add(bufferTime).After(expiration)
64
65 return expiresWithinBuffer, nil
66}