diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index a140a87..3fe0e80 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -3,6 +3,7 @@ package auth import ( "context" "fmt" + "sync" "time" "github.com/MicahParks/keyfunc/v2" @@ -13,33 +14,48 @@ import ( type Validator struct { issuer string audience string + jwksURL string jwks *keyfunc.JWKS + mu sync.Mutex } // NewValidator creates a new JWT validator for a service. +// JWKS fetching is lazy (deferred until first validation). func NewValidator(issuer, audience, jwksURL string) *Validator { - // Create JWKS from URL with automatic refresh + return &Validator{ + issuer: issuer, + audience: audience, + jwksURL: jwksURL, + jwks: nil, // Lazy-loaded on first use + } +} + +// ensureJWKS fetches JWKS on first use (lazy initialization, thread-safe). +func (v *Validator) ensureJWKS() error { + v.mu.Lock() + defer v.mu.Unlock() + + if v.jwks != nil { + return nil + } + options := keyfunc.Options{ Ctx: context.Background(), RefreshInterval: 15 * time.Minute, RefreshRateLimit: 5 * time.Minute, RefreshTimeout: 10 * time.Second, RefreshErrorHandler: func(err error) { - // Log refresh errors but don't fail - fmt.Printf("JWKS refresh error for %s: %v\n", issuer, err) + fmt.Printf("JWKS refresh error for %s: %v\n", v.issuer, err) }, } - jwks, err := keyfunc.Get(jwksURL, options) + jwks, err := keyfunc.Get(v.jwksURL, options) if err != nil { - panic(fmt.Sprintf("failed to fetch JWKS from %s: %v", jwksURL, err)) + return fmt.Errorf("failed to fetch JWKS from %s: %v", v.jwksURL, err) } - return &Validator{ - issuer: issuer, - audience: audience, - jwks: jwks, - } + v.jwks = jwks + return nil } // ValidateBearerToken extracts and validates the Bearer token from Authorization header. @@ -57,6 +73,11 @@ func (v *Validator) ValidateBearerToken(authHeader string) (jwt.MapClaims, error return nil, fmt.Errorf("invalid Authorization header format") } + // Ensure JWKS is loaded (lazy) + if err := v.ensureJWKS(); err != nil { + return nil, err + } + // Parse and validate claims := jwt.MapClaims{} token, err := jwt.ParseWithClaims(tokenString, claims, v.jwks.Keyfunc)