feat: Phase 3.1 - SQS JWT validation against Authentik JWKS
CI / Vet, test, build (push) Successful in 2m4s
CI / Build and push image (push) Failing after 18s

Implements gateway-level JWT validation for SQS requests:
- Validates JWT signature against Authentik JWKS
- Verifies claims: iss, aud, exp, nbf (with 60s skew)
- Checks 'permissions' claim for sqs:read/sqs:write/wildcard
- Returns 403 with error details on validation failure
- JWKS caching with 15min TTL and auto-refresh on key rotation

Architecture:
- SQS: Gateway validates JWT (kmsvc code unverified)
- MinIO, Temporal: Native JWT support (pass-through)
- Memory, IAM: Service-owned JWT validation

Integration tests added:
- Reject requests without Authorization header (403)
- Accept requests with valid JWT from Authentik
- Pass through Authorization header unchanged for other services

Uses github.com/MicahParks/keyfunc/v2 for JWKS handling:
- Automatic refresh every 15 minutes
- On-demand refresh if kid not found
- Handles RS256 signatures
This commit is contained in:
Admin Bot
2026-08-27 11:40:35 -07:00
parent 55b32b97e0
commit 9d9395d938
5 changed files with 251 additions and 17 deletions
+33 -4
View File
@@ -13,6 +13,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/auth"
"forgejo.riotpiao.com/rock/homelab-frontend/internal/problem"
)
@@ -22,13 +23,23 @@ import (
// MinIO, Temporal: Native JWT support (dumb pipe pass-through)
// Memory, IAM: Services validate JWTs themselves
type Dispatcher struct {
registry *Registry
registry *Registry
sqsJWTAuth *auth.Validator
}
// NewDispatcher creates a new service adapter dispatcher.
func NewDispatcher(registry *Registry) *Dispatcher {
// Create JWT validator for SQS
// Issuer and JWKS URL should match Authentik application config
sqsValidator := auth.NewValidator(
"https://authentik.riotpiao.com/application/o/sqs/",
"sqs",
"https://authentik.riotpiao.com/application/o/sqs/jwks/",
)
return &Dispatcher{
registry: registry,
registry: registry,
sqsJWTAuth: sqsValidator,
}
}
@@ -94,13 +105,31 @@ func (d *Dispatcher) Dispatch(w http.ResponseWriter, r *http.Request) {
// Gateway-level JWT validation for SQS (code unverified in kmsvc)
// MinIO, Temporal, Memory, IAM have native JWT support - pass through
if adapter.Spec.Auth.Required && serviceName == "sqs" {
if r.Header.Get("Authorization") == "" {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
"Forbidden", "SQS requires Authorization header")
_ = p.Write(w)
return
}
// TODO: Phase 3 - validate JWT signature against Authentik JWKS for SQS
// Validate JWT signature against Authentik JWKS
claims, err := d.sqsJWTAuth.ValidateBearerToken(authHeader)
if err != nil {
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
"Forbidden", fmt.Sprintf("JWT validation failed: %v", err))
_ = p.Write(w)
return
}
// Check required permissions (sqs:read or sqs:write or *)
hasPermission := d.sqsJWTAuth.CheckPermissions(claims, "sqs:read", "sqs:write", "*")
if !hasPermission {
p := problem.NewProblem(http.StatusForbidden, "about:blank#forbidden",
"Forbidden", "Insufficient permissions for SQS")
_ = p.Write(w)
return
}
}
// Detect protocol from upstream URL scheme