mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
mana-search-go → mana-search mana-notify-go → mana-notify mana-crawler-go → mana-crawler mana-api-gateway-go → mana-api-gateway Legacy NestJS versions are deleted, suffix no longer needed. Updated all references in docker-compose, CLAUDE.md, package.json, Forgejo workflows, and service package.json files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
type User struct {
|
|
UserID string `json:"userId"`
|
|
Email string `json:"email"`
|
|
Role string `json:"role"`
|
|
SessionID string `json:"sessionId"`
|
|
}
|
|
|
|
type contextKey string
|
|
|
|
const UserContextKey contextKey = "user"
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ValidateJWT validates Bearer tokens against mana-core-auth JWKS.
|
|
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))
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|