tercul-backend/internal/platform/auth/middleware.go
google-labs-jules[bot] f675c98e80 Fix: Correct authorization logic in integration tests
The integration tests for admin-only mutations were failing due to an authorization issue. The root cause was that the JWT token used in the tests did not reflect the user's admin role, which was being set directly in the database.

This commit fixes the issue by:
1.  Updating the `CreateAuthenticatedUser` test helper to generate a new JWT token after a user's role is changed. This ensures the token contains the correct, up-to-date role.
2.  Removing all uses of `auth.ContextWithAdminUser` from the integration tests, making the JWT token the single source of truth for authorization.

This change also removes unused imports and variables that were causing build failures after the refactoring. All integration tests now pass.
2025-10-04 23:48:44 +00:00

201 lines
5.4 KiB
Go

package auth
import (
"context"
"net/http"
"strings"
"tercul/internal/platform/log"
)
// ContextKey is a type for context keys
type ContextKey string
const (
// UserContextKey is the key for user in context
UserContextKey ContextKey = "user"
// ClaimsContextKey is the key for claims in context
ClaimsContextKey ContextKey = "claims"
)
// AuthMiddleware creates middleware for JWT authentication
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip authentication for certain paths
if shouldSkipAuth(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
// Extract token from Authorization header
authHeader := r.Header.Get("Authorization")
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
if err != nil {
log.LogWarn("Authentication failed - missing or invalid token",
log.F("path", r.URL.Path),
log.F("error", err))
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
log.LogWarn("Authentication failed - invalid token",
log.F("path", r.URL.Path),
log.F("error", err))
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Add claims to context
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RoleMiddleware creates middleware for role-based authorization
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
if !ok {
log.LogWarn("Authorization failed - no claims in context",
log.F("path", r.URL.Path),
log.F("required_role", requiredRole))
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
log.LogWarn("Authorization failed - insufficient role",
log.F("path", r.URL.Path),
log.F("user_role", claims.Role),
log.F("required_role", requiredRole))
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// GraphQLAuthMiddleware creates middleware specifically for GraphQL requests
func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
next.ServeHTTP(w, r)
return
}
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
if err != nil {
log.LogWarn("GraphQL authentication failed - could not extract token", log.F("error", err))
next.ServeHTTP(w, r) // Proceed without auth
return
}
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
log.LogWarn("GraphQL authentication failed - invalid token", log.F("error", err))
next.ServeHTTP(w, r) // Proceed without auth
return
}
// Add claims to context for authenticated requests
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetClaimsFromContext extracts claims from context
func GetClaimsFromContext(ctx context.Context) (*Claims, bool) {
claims, ok := ctx.Value(ClaimsContextKey).(*Claims)
return claims, ok
}
// GetUserIDFromContext extracts user ID from context
func GetUserIDFromContext(ctx context.Context) (uint, bool) {
claims, ok := GetClaimsFromContext(ctx)
if !ok {
return 0, false
}
return claims.UserID, true
}
// IsAuthenticated checks if the request is authenticated
func IsAuthenticated(ctx context.Context) bool {
_, ok := GetClaimsFromContext(ctx)
return ok
}
// RequireAuth ensures the request is authenticated
func RequireAuth(ctx context.Context) (*Claims, error) {
claims, ok := GetClaimsFromContext(ctx)
if !ok {
return nil, ErrMissingToken
}
return claims, nil
}
// RequireRole ensures the user has the required role
func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) {
claims, err := RequireAuth(ctx)
if err != nil {
return nil, err
}
jwtManager := NewJWTManager()
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
return nil, err
}
return claims, nil
}
// shouldSkipAuth determines if authentication should be skipped for a path
func shouldSkipAuth(path string) bool {
skipPaths := []string{
"/",
"/query",
"/health",
"/metrics",
"/favicon.ico",
}
for _, skipPath := range skipPaths {
if path == skipPath {
return true
}
}
// Skip static files
if strings.HasPrefix(path, "/static/") {
return true
}
return false
}
// ContextWithUserID adds a user ID to the context for testing purposes.
func ContextWithUserID(ctx context.Context, userID uint) context.Context {
claims := &Claims{UserID: userID}
return context.WithValue(ctx, ClaimsContextKey, claims)
}
// ContextWithAdminUser adds an admin user to the context for testing purposes.
func ContextWithAdminUser(ctx context.Context, userID uint) context.Context {
claims := &Claims{
UserID: userID,
Role: "admin",
}
return context.WithValue(ctx, ClaimsContextKey, claims)
}