mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 11:46:43 +02:00
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>
This commit is contained in:
parent
509a541b70
commit
4f70e1ca6c
14 changed files with 466 additions and 385 deletions
|
|
@ -3,9 +3,8 @@ module github.com/manacore/mana-notify
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/manacore/shared-go v0.0.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/manacore/shared-go v0.0.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/rs/cors v1.11.1
|
||||
)
|
||||
|
|
@ -13,6 +12,7 @@ require (
|
|||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
|
|
|
|||
|
|
@ -1,126 +1,31 @@
|
|||
// Package auth provides authentication middleware for mana-notify.
|
||||
// Delegates to shared-go/authutil for JWT and service key validation.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/manacore/shared-go/authutil"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
// Re-export types for backward compatibility.
|
||||
type User = authutil.User
|
||||
|
||||
type contextKey string
|
||||
|
||||
const UserContextKey contextKey = "user"
|
||||
// UserContextKey is the context key for the authenticated user.
|
||||
const UserContextKey = authutil.UserContextKey
|
||||
|
||||
// ValidateServiceKey checks the X-Service-Key header.
|
||||
func ValidateServiceKey(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)
|
||||
})
|
||||
}
|
||||
return authutil.ServiceKeyMiddleware(serviceKey)
|
||||
}
|
||||
|
||||
// ValidateJWT validates Bearer tokens against mana-core-auth JWKS.
|
||||
// ValidateJWT validates Bearer tokens against mana-core-auth.
|
||||
func ValidateJWT(authURL string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(header, "Bearer ") {
|
||||
http.Error(w, `{"error":"unauthorized: missing token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenStr := strings.TrimPrefix(header, "Bearer ")
|
||||
|
||||
// Validate against auth service
|
||||
user, err := validateToken(r.Context(), authURL, tokenStr)
|
||||
if err != nil {
|
||||
slog.Warn("jwt validation failed", "error", err)
|
||||
http.Error(w, `{"error":"unauthorized: invalid token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), UserContextKey, user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
validator := authutil.NewRemoteValidator(authURL)
|
||||
return authutil.JWTMiddleware(validator)
|
||||
}
|
||||
|
||||
// GetUser extracts the authenticated user from context.
|
||||
func GetUser(r *http.Request) *User {
|
||||
u, ok := r.Context().Value(UserContextKey).(*User)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func validateToken(ctx context.Context, authURL, tokenStr string) (*User, error) {
|
||||
// Parse without verification first to get claims
|
||||
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||
token, _, err := parser.ParseUnverified(tokenStr, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse token: %w", err)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid claims")
|
||||
}
|
||||
|
||||
// Validate via auth service
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, authURL+"/api/v1/auth/validate", strings.NewReader(`{"token":"`+tokenStr+`"}`))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("auth service unavailable: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("token validation failed: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
if !result.Valid {
|
||||
return nil, fmt.Errorf("token invalid")
|
||||
}
|
||||
|
||||
sub, _ := claims["sub"].(string)
|
||||
email, _ := claims["email"].(string)
|
||||
role, _ := claims["role"].(string)
|
||||
sid, _ := claims["sid"].(string)
|
||||
|
||||
return &User{
|
||||
UserID: sub,
|
||||
Email: email,
|
||||
Role: role,
|
||||
SessionID: sid,
|
||||
}, nil
|
||||
return authutil.GetUser(r)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue