package auth import ( "context" "net/http" "strings" "tercul/logger" ) // 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 { logger.LogWarn("Authentication failed - missing or invalid token", logger.F("path", r.URL.Path), logger.F("error", err)) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Validate token claims, err := jwtManager.ValidateToken(tokenString) if err != nil { logger.LogWarn("Authentication failed - invalid token", logger.F("path", r.URL.Path), logger.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 { logger.LogWarn("Authorization failed - no claims in context", logger.F("path", r.URL.Path), logger.F("required_role", requiredRole)) http.Error(w, "Forbidden", http.StatusForbidden) return } jwtManager := NewJWTManager() if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil { logger.LogWarn("Authorization failed - insufficient role", logger.F("path", r.URL.Path), logger.F("user_role", claims.Role), logger.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) { // For GraphQL, we want to authenticate but not block requests // This allows for both authenticated and anonymous queries authHeader := r.Header.Get("Authorization") if authHeader != "" { tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader) if err == nil { claims, err := jwtManager.ValidateToken(tokenString) if err == nil { // Add claims to context for authenticated requests ctx := context.WithValue(r.Context(), ClaimsContextKey, claims) next.ServeHTTP(w, r.WithContext(ctx)) return } } // If token is invalid, log warning but continue logger.LogWarn("GraphQL authentication failed - continuing with anonymous access", logger.F("path", r.URL.Path)) } // Continue without authentication next.ServeHTTP(w, r) }) } } // 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 }