package middleware_test // token_confusion_test.go — Pre-fix tests for cross-type JWT token confusion. // // BUG F-A3-12 MEDIUM: Agent and web JWTs share the same signing secret with // no issuer/audience differentiation. Cross-type token confusion is possible. // // The shared secret is set at main.go:166. Both AuthMiddleware (agent JWT) // and WebAuthMiddleware (admin JWT) use the same HMAC key. Without issuer // or audience claims, a JWT valid for one context may pass signature // validation in the other. // // Run: cd aggregator-server && go test ./internal/api/middleware/... -v -run TestToken import ( "net/http" "net/http/httptest" "testing" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/api/handlers" "github.com/Fimeg/RedFlag/aggregator-server/internal/api/middleware" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" ) // makeWebJWT creates a valid web/admin JWT for testing func makeWebJWT(t *testing.T, secret string) string { t.Helper() claims := handlers.UserClaims{ UserID: "1", Username: "admin", Role: "admin", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(secret)) if err != nil { t.Fatalf("failed to sign web JWT: %v", err) } return signed } // --------------------------------------------------------------------------- // Test 4.1 — Web token SHOULD be rejected by agent AuthMiddleware // // Category: Verify actual behavior — PASS or FAIL depending on claims parsing // // BUG F-A3-12: Shared JWT secret allows cross-type token use. A web token // passes signature validation on agent middleware. The question is whether // claims parsing (AgentClaims expecting AgentID uuid.UUID) rejects the // web token that has UserID string instead. // --------------------------------------------------------------------------- func TestWebTokenRejectedByAgentAuthMiddleware(t *testing.T) { sharedSecret := "shared-secret-confusion-test" middleware.JWTSecret = sharedSecret router := gin.New() router.Use(middleware.AuthMiddleware()) router.GET("/agent-route", func(c *gin.Context) { agentID, exists := c.Get("agent_id") if !exists { c.JSON(http.StatusInternalServerError, gin.H{"error": "no agent_id"}) return } c.JSON(http.StatusOK, gin.H{"agent_id": agentID}) }) // Create a web JWT (UserClaims with UserID, Username, Role) webToken := makeWebJWT(t, sharedSecret) req := httptest.NewRequest("GET", "/agent-route", nil) req.Header.Set("Authorization", "Bearer "+webToken) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) // A web token SHOULD be rejected by agent middleware. // If the claims parsing is strict enough, it will return 401. // If not, the token passes — documenting the confusion risk. if rec.Code != http.StatusUnauthorized && rec.Code != http.StatusForbidden { t.Errorf("[ERROR] [server] [auth] web JWT accepted by agent AuthMiddleware (got %d).\n"+ "BUG F-A3-12: cross-type token confusion — web token passes agent auth.\n"+ "After fix: add issuer/audience claims or use separate signing secrets.", rec.Code) } else { t.Logf("[INFO] [server] [auth] web JWT rejected by agent AuthMiddleware (%d) — claims parsing caught it", rec.Code) } } // --------------------------------------------------------------------------- // Test 4.2 — Agent token SHOULD be rejected by WebAuthMiddleware // // Category: Verify actual behavior — PASS or FAIL depending on claims parsing // // BUG F-A3-12: An agent token (AgentClaims with AgentID uuid.UUID) may pass // signature validation on WebAuthMiddleware. The question is whether the // UserClaims parsing rejects it. // --------------------------------------------------------------------------- func TestAgentTokenRejectedByWebAuthMiddleware(t *testing.T) { sharedSecret := "shared-secret-confusion-test-2" middleware.JWTSecret = sharedSecret authHandler := handlers.NewAuthHandler(sharedSecret, nil) router := gin.New() router.Use(authHandler.WebAuthMiddleware()) router.GET("/admin-route", func(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusInternalServerError, gin.H{"error": "no user_id"}) return } c.JSON(http.StatusOK, gin.H{"user_id": userID}) }) // Create an agent JWT (AgentClaims with AgentID uuid.UUID) agentClaims := middleware.AgentClaims{ AgentID: uuid.New(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, agentClaims) agentToken, err := token.SignedString([]byte(sharedSecret)) if err != nil { t.Fatalf("failed to sign agent JWT: %v", err) } req := httptest.NewRequest("GET", "/admin-route", nil) req.Header.Set("Authorization", "Bearer "+agentToken) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) // An agent token SHOULD be rejected by web middleware. if rec.Code != http.StatusUnauthorized && rec.Code != http.StatusForbidden { t.Errorf("[ERROR] [server] [auth] agent JWT accepted by WebAuthMiddleware (got %d).\n"+ "BUG F-A3-12: cross-type token confusion — agent token passes admin auth.\n"+ "After fix: add issuer/audience claims or use separate signing secrets.", rec.Code) } else { t.Logf("[INFO] [server] [auth] agent JWT rejected by WebAuthMiddleware (%d) — claims parsing caught it", rec.Code) } }