From e0fd5cea8c8dc3401c64fdd9c180b571a430fa8a Mon Sep 17 00:00:00 2001 From: Patrick Britton Date: Mon, 16 Feb 2026 12:22:02 -0600 Subject: [PATCH] Fix Turnstile verification encoding and admin login diagnostics --- go-backend/internal/handlers/admin_handler.go | 15 ++++++--- .../internal/services/turnstile_service.go | 33 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/go-backend/internal/handlers/admin_handler.go b/go-backend/internal/handlers/admin_handler.go index df3d53a..b4d2825 100644 --- a/go-backend/internal/handlers/admin_handler.go +++ b/go-backend/internal/handlers/admin_handler.go @@ -83,17 +83,24 @@ func (h *AdminHandler) AdminLogin(c *gin.Context) { // Verify Turnstile token if h.turnstileSecret != "" { + if strings.TrimSpace(req.TurnstileToken) == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Security verification failed"}) + return + } turnstileService := services.NewTurnstileService(h.turnstileSecret) - remoteIP := c.ClientIP() - turnstileResp, err := turnstileService.VerifyToken(req.TurnstileToken, remoteIP) + turnstileResp, err := turnstileService.VerifyToken(req.TurnstileToken, "") if err != nil { log.Error().Err(err).Msg("Admin login: Turnstile verification failed") c.JSON(http.StatusBadRequest, gin.H{"error": "Security verification failed"}) return } if !turnstileResp.Success { - log.Warn().Strs("errors", turnstileResp.ErrorCodes).Msg("Admin login: Turnstile validation failed") - c.JSON(http.StatusForbidden, gin.H{"error": "Security verification failed. Please try again."}) + log.Warn(). + Strs("errors", turnstileResp.ErrorCodes). + Str("hostname", turnstileResp.Hostname). + Str("action", turnstileResp.Action). + Msg("Admin login: Turnstile validation failed") + c.JSON(http.StatusBadRequest, gin.H{"error": "Security verification failed"}) return } } diff --git a/go-backend/internal/services/turnstile_service.go b/go-backend/internal/services/turnstile_service.go index 30967b8..4e78758 100644 --- a/go-backend/internal/services/turnstile_service.go +++ b/go-backend/internal/services/turnstile_service.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "time" ) @@ -39,22 +40,18 @@ func (s *TurnstileService) VerifyToken(token, remoteIP string) (*TurnstileRespon return &TurnstileResponse{Success: true}, nil } - // Prepare the request data - data := fmt.Sprintf( - "secret=%s&response=%s", - s.secretKey, - token, - ) - - if remoteIP != "" { - data += fmt.Sprintf("&remoteip=%s", remoteIP) - } + // Prepare the request data (properly form-encoded) + // Note: We intentionally do NOT send remoteip. In practice this often causes false negatives + // behind proxies/CDNs (Cloudflare), and Turnstile does not require it. + form := url.Values{} + form.Set("secret", s.secretKey) + form.Set("response", token) // Make the request to Cloudflare resp, err := s.client.Post( "https://challenges.cloudflare.com/turnstile/v0/siteverify", "application/x-www-form-urlencoded", - bytes.NewBufferString(data), + bytes.NewBufferString(form.Encode()), ) if err != nil { return nil, fmt.Errorf("failed to verify turnstile token: %w", err) @@ -79,13 +76,13 @@ func (s *TurnstileService) VerifyToken(token, remoteIP string) (*TurnstileRespon // GetErrorMessage returns a user-friendly error message for error codes func (s *TurnstileService) GetErrorMessage(errorCodes []string) string { errorMessages := map[string]string{ - "missing-input-secret": "Server configuration error", - "invalid-input-secret": "Server configuration error", - "missing-input-response": "Please complete the security check", - "invalid-input-response": "Security check failed, please try again", - "bad-request": "Invalid request format", - "timeout-or-duplicate": "Security check expired, please try again", - "internal-error": "Verification service unavailable", + "missing-input-secret": "Server configuration error", + "invalid-input-secret": "Server configuration error", + "missing-input-response": "Please complete the security check", + "invalid-input-response": "Security check failed, please try again", + "bad-request": "Invalid request format", + "timeout-or-duplicate": "Security check expired, please try again", + "internal-error": "Verification service unavailable", } for _, code := range errorCodes {