From 8dfd17127b2e6766edafc1546412f91087d9a6ba Mon Sep 17 00:00:00 2001 From: Admin Bot Date: Thu, 27 Aug 2026 11:16:40 -0700 Subject: [PATCH] chore: remove unused internal/auth package JWT validation moved to individual services (Option B). Gateway no longer needs auth module. --- internal/auth/jwt.go | 198 ------------------------------------------- 1 file changed, 198 deletions(-) delete mode 100644 internal/auth/jwt.go diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go deleted file mode 100644 index f02220d..0000000 --- a/internal/auth/jwt.go +++ /dev/null @@ -1,198 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - "sync" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -// Validator validates JWT tokens from Authentik. -type Validator struct { - issuer string - audience string - jwksURL string - client *http.Client - - mu sync.RWMutex - keyset *keySet - lastFetch time.Time - cacheTTL time.Duration -} - -type keySet struct { - Keys map[string]interface{} `json:"keys"` -} - -// Claims holds Authentik JWT claims. -type Claims struct { - jwt.RegisteredClaims - Permissions []string `json:"permissions"` - Groups []string `json:"groups"` -} - -// NewValidator creates a validator for an Authentik app. -// appSlug is the OAuth2 provider slug (e.g., "gateway", "sqs", "memory"). -func NewValidator(appSlug string) *Validator { - issuer := fmt.Sprintf("https://authentik.riotpiao.com/application/o/%s/", appSlug) - return &Validator{ - issuer: issuer, - audience: appSlug, - jwksURL: issuer + "jwks/", - client: &http.Client{Timeout: 10 * time.Second}, - cacheTTL: 15 * time.Minute, - } -} - -// ValidateToken extracts and validates a JWT from the Authorization header. -// Returns the claims if valid, or an error if invalid/missing. -func (v *Validator) ValidateToken(authHeader string) (*Claims, error) { - // Extract token from "Bearer " - if authHeader == "" { - return nil, fmt.Errorf("authorization header missing") - } - - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - return nil, fmt.Errorf("invalid authorization header format") - } - - tokenString := parts[1] - - // Ensure JWKS is fresh - if err := v.ensureKeys(); err != nil { - return nil, fmt.Errorf("failed to fetch JWKS: %w", err) - } - - // Parse JWT with custom key func - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - // Verify algorithm is RS256 only - if token.Method.Alg() != "RS256" { - return nil, fmt.Errorf("unexpected algorithm: %v", token.Header["alg"]) - } - - kid, ok := token.Header["kid"].(string) - if !ok { - return nil, fmt.Errorf("kid not in token header") - } - - // Get key from cache - v.mu.RLock() - keys := v.keyset.Keys - v.mu.RUnlock() - - rawKey, exists := keys[kid] - if !exists { - // Try refreshing JWKS (key rotation) - if err := v.fetchKeys(); err == nil { - v.mu.RLock() - rawKey, exists = v.keyset.Keys[kid] - v.mu.RUnlock() - } - if !exists { - return nil, fmt.Errorf("key %s not found", kid) - } - } - - return rawKey, nil - }) - - if err != nil { - return nil, fmt.Errorf("token validation failed: %w", err) - } - - if !token.Valid { - return nil, fmt.Errorf("token invalid") - } - - claims, ok := token.Claims.(*Claims) - if !ok { - return nil, fmt.Errorf("invalid claims") - } - - // Verify issuer - if claims.Issuer != v.issuer { - return nil, fmt.Errorf("issuer mismatch") - } - - // Verify audience (check if audience is in the claims) - if len(claims.Audience) == 0 { - return nil, fmt.Errorf("no audience in token") - } - audFound := false - for _, aud := range claims.Audience { - if aud == v.audience { - audFound = true - break - } - } - if !audFound { - return nil, fmt.Errorf("audience mismatch: expected %s", v.audience) - } - - // Verify exp, nbf, iat - now := time.Now().Unix() - if claims.ExpiresAt != nil && claims.ExpiresAt.Unix() < now { - return nil, fmt.Errorf("token expired") - } - if claims.NotBefore != nil && claims.NotBefore.Unix() > now+60 { - return nil, fmt.Errorf("token not yet valid") - } - - return claims, nil -} - -// HasPermission checks if claims contain the required permission. -// Wildcard "*" grants all permissions. -func (v *Validator) HasPermission(claims *Claims, permission string) bool { - for _, p := range claims.Permissions { - if p == "*" || p == permission { - return true - } - } - return false -} - -// ensureKeys refreshes JWKS if cache is stale. -func (v *Validator) ensureKeys() error { - v.mu.RLock() - cacheValid := v.keyset != nil && time.Since(v.lastFetch) < v.cacheTTL - v.mu.RUnlock() - - if cacheValid { - return nil - } - - return v.fetchKeys() -} - -// fetchKeys fetches JWKS from Authentik. -func (v *Validator) fetchKeys() error { - resp, err := v.client.Get(v.jwksURL) - if err != nil { - return fmt.Errorf("GET %s failed: %w", v.jwksURL, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("JWKS endpoint returned %d: %s", resp.StatusCode, string(body)) - } - - var ks keySet - if err := json.NewDecoder(resp.Body).Decode(&ks); err != nil { - return fmt.Errorf("decode JWKS failed: %w", err) - } - - v.mu.Lock() - v.keyset = &ks - v.lastFetch = time.Now() - v.mu.Unlock() - - return nil -}