feat(auth): wire JWT validation into /v1/* LLM endpoints
CI / Vet, test, build (push) Successful in 3m52s
CI / Build and push image (push) Successful in 1m17s

This commit is contained in:
Admin Bot
2026-08-31 23:01:59 -07:00
parent f9addf945d
commit 14cc67833c
5 changed files with 141 additions and 29 deletions
+57
View File
@@ -13,6 +13,7 @@ import (
"strings"
"time"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/logging"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/tracing"
@@ -25,6 +26,8 @@ type Handler struct {
transports map[string]*http.Transport
// config holds the gateway configuration (for model registry, etc.)
config *config.Config
// jwtValidator validates JWT tokens for authenticated endpoints
jwtValidator *auth.Validator
// Default timeouts for synthesized routes (model-based dispatch)
defaultConnectTimeout time.Duration
defaultReadTimeout time.Duration
@@ -73,6 +76,15 @@ func New(cfg *config.Config) *Handler {
defaultMaxBodySize: 100 * 1024 * 1024,
}
// Initialize JWT validator if auth is enabled
if cfg.Auth.Enabled && cfg.Auth.JWKSURL != "" {
h.jwtValidator = auth.NewValidator(
cfg.Auth.Issuer,
cfg.Auth.Audience,
cfg.Auth.JWKSURL,
)
}
for name, route := range cfg.Routes {
// Create a transport per unique upstream address for connection reuse
transport := h.getOrCreateTransport(route.Upstream.Address, &route.Upstream)
@@ -291,6 +303,51 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
// JWT Authentication for /v1/* endpoints
if h.jwtValidator != nil && strings.HasPrefix(r.URL.Path, "/v1/") {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
writeProblemDetail(w, http.StatusUnauthorized,
"https://api.example.com/problems/unauthorized",
"Unauthorized",
"Authorization header required",
nil)
logging.Errorf("auth failed", fmt.Errorf("missing auth header"), map[string]string{
"path": r.URL.Path,
})
return
}
claims, err := h.jwtValidator.ValidateBearerToken(authHeader)
if err != nil {
writeProblemDetail(w, http.StatusForbidden,
"https://api.example.com/problems/forbidden",
"Forbidden",
fmt.Sprintf("JWT validation failed: %v", err),
nil)
logging.Errorf("auth failed", err, map[string]string{
"path": r.URL.Path,
})
return
}
// Check required capability if configured
if h.config.Auth.RequiredCapability != "" {
if !h.jwtValidator.CheckPermissions(claims, h.config.Auth.RequiredCapability, "*") {
writeProblemDetail(w, http.StatusForbidden,
"https://api.example.com/problems/insufficient-permissions",
"Insufficient Permissions",
fmt.Sprintf("Required capability: %s", h.config.Auth.RequiredCapability),
nil)
logging.Errorf("auth failed", fmt.Errorf("insufficient permissions"), map[string]string{
"path": r.URL.Path,
"required": h.config.Auth.RequiredCapability,
})
return
}
}
}
// Note: Body size checking already happened in RouteRequest (body was read for model dispatch).
// For other paths, we still need to enforce the cap.
// For /v1/chat/completions, the body was already read and validated.