managarten/packages/shared-go/authutil/middleware.go
Till JS 4f70e1ca6c refactor(shared-go): extract shared auth package from 3 Go services
Create packages/shared-go/authutil/ with two JWT validator implementations:
- JWKSValidator: EdDSA JWKS validation with key caching (extracted from mana-sync)
- RemoteValidator: delegates to mana-core-auth /api/v1/auth/validate (from mana-notify/gateway)

Plus shared types (Claims, User), middleware factories (JWTMiddleware, ServiceKeyMiddleware),
context helpers (GetUser, GetUserID, GetUserRole), and token extraction.

Migrated services:
- mana-sync: internal/auth/jwt.go now wraps authutil.JWKSValidator
- mana-notify: internal/auth/auth.go now wraps authutil.RemoteValidator + ServiceKeyMiddleware
- mana-api-gateway: internal/middleware/jwt.go now wraps authutil.RemoteValidator

All 3 services compile and pass tests. Service-level packages re-export types
for backward compatibility so no consumer code changes are needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:27:44 +02:00

86 lines
2.7 KiB
Go

package authutil
import (
"context"
"log/slog"
"net/http"
)
type contextKey string
const (
// UserContextKey stores the full *User in context.
UserContextKey contextKey = "user"
// UserIDContextKey stores just the user ID string in context.
UserIDContextKey contextKey = "userID"
// UserRoleContextKey stores the user role string in context.
UserRoleContextKey contextKey = "userRole"
)
// TokenValidator is the interface both JWKSValidator and RemoteValidator implement.
type TokenValidator interface {
ValidateToken(tokenStr string) (*Claims, error)
}
// JWTMiddleware returns HTTP middleware that validates Bearer tokens using the given validator.
// On success, the authenticated User is stored in context (accessible via GetUser/GetUserID/GetUserRole).
func JWTMiddleware(validator TokenValidator) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := ExtractToken(r)
if tokenStr == "" {
http.Error(w, `{"error":"unauthorized: missing token"}`, http.StatusUnauthorized)
return
}
claims, err := validator.ValidateToken(tokenStr)
if err != nil {
slog.Warn("jwt validation failed", "error", err)
http.Error(w, `{"error":"unauthorized: invalid token"}`, http.StatusUnauthorized)
return
}
user := UserFromClaims(claims)
ctx := r.Context()
ctx = context.WithValue(ctx, UserContextKey, user)
ctx = context.WithValue(ctx, UserIDContextKey, user.UserID)
ctx = context.WithValue(ctx, UserRoleContextKey, user.Role)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ServiceKeyMiddleware returns HTTP middleware that validates the X-Service-Key header.
func ServiceKeyMiddleware(serviceKey string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-Service-Key")
if key == "" || key != serviceKey {
http.Error(w, `{"error":"unauthorized: invalid service key"}`, http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
// GetUser extracts the authenticated User from the request context.
func GetUser(r *http.Request) *User {
u, ok := r.Context().Value(UserContextKey).(*User)
if !ok {
return nil
}
return u
}
// GetUserID extracts the user ID string from the request context.
func GetUserID(r *http.Request) string {
id, _ := r.Context().Value(UserIDContextKey).(string)
return id
}
// GetUserRole extracts the user role string from the request context.
func GetUserRole(r *http.Request) string {
role, _ := r.Context().Value(UserRoleContextKey).(string)
return role
}