feat: add JWT validation against Authentik JWKS for protected adapters
CI / Vet, test, build (push) Successful in 2m4s
CI / Build and push image (push) Successful in 50s

Replaces stub 'check Authorization header' auth with real JWT validation:
- Extracts Bearer token from Authorization header
- Validates signature against Authentik JWKS endpoint
- Verifies iss, aud, exp claims
- Checks permissions claim for required capability
- Handles key rotation with 15min cache TTL
- Returns 403 with detailed error on auth failure

Protected adapters (memory, iam) now require valid Authentik JWT tokens.
This commit is contained in:
Admin Bot
2026-08-27 11:07:20 -07:00
parent 46dc24a26c
commit df33203a72
4 changed files with 238 additions and 19 deletions
+37 -19
View File
@@ -8,21 +8,29 @@ import (
"net/url"
"time"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/problem"
)
// Dispatcher handles X-Service based routing to service adapters.
type Dispatcher struct {
registry *Registry
// authValidator would check capabilities if internal/auth exists
// For now, we stub it
registry *Registry
validators map[string]*auth.Validator // Per-service JWT validators
}
// NewDispatcher creates a new service adapter dispatcher.
// NewDispatcher creates a new service adapter dispatcher with Authentik JWT validators.
func NewDispatcher(registry *Registry) *Dispatcher {
return &Dispatcher{
registry: registry,
dispatcher := &Dispatcher{
registry: registry,
validators: make(map[string]*auth.Validator),
}
// Create validators for all registered services
for _, adapter := range registry.List() {
dispatcher.validators[adapter.ServiceName] = auth.NewValidator(adapter.ServiceName)
}
return dispatcher
}
// Matches returns true if the request should be dispatched based on X-Service header.
@@ -85,8 +93,7 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
return
}
// Check auth requirements (stub for now — internal/auth integration in 8.3)
// Determine required capability
// Check auth requirements using JWT validation
requiredCapability := ""
auth := resource.Auth
if auth == nil {
@@ -98,11 +105,28 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
if auth != nil && auth.Required && auth.Capability != "" {
requiredCapability = auth.Capability
// Would validate JWT and capability here (depends on internal/auth)
// For now, stub — just log that it would be checked
if !d.hasCapability(r, requiredCapability) {
// Validate JWT token
validator := d.validators[serviceName]
if validator == nil {
p := problem.NewProblem(http.StatusInternalServerError, "about:blank#server-error",
"Internal Server Error", fmt.Sprintf("no validator for service '%s'", serviceName))
_ = p.Write(w)
return
}
claims, err := validator.ValidateToken(r.Header.Get("Authorization"))
if err != nil {
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
"Forbidden", fmt.Sprintf("capability '%s' required", requiredCapability))
"Forbidden", fmt.Sprintf("authentication failed: %v", err))
_ = p.Write(w)
return
}
// Check permission
if !validator.HasPermission(claims, requiredCapability) {
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
"Forbidden", fmt.Sprintf("permission '%s' required", requiredCapability))
_ = p.Write(w)
return
}
@@ -140,13 +164,7 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
// hasCapability checks if the request has the required capability.
// Stub implementation — depends on internal/auth JWT validation.
func (d *Dispatcher) hasCapability(r *http.Request, capability string) bool {
// TODO: Parse JWT from Authorization header and check capabilities
// For now, assume all authenticated requests have all capabilities
return r.Header.Get("Authorization") != ""
}
func (d *Dispatcher) writeError(w http.ResponseWriter, p *problem.Problem) {
_ = p.Write(w)