mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 02:51:34 +00:00
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.
201 lines
5.4 KiB
Go
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)
|
|
}
|