diff --git a/internal/handlers/auth_csrf_rl_authp0_test.go b/internal/handlers/auth_csrf_rl_authp0_test.go new file mode 100644 index 0000000..1de8c9c --- /dev/null +++ b/internal/handlers/auth_csrf_rl_authp0_test.go @@ -0,0 +1,191 @@ +package handlers + +// auth_csrf_rl_authp0_test.go — regression tests for AUTH-163, AUTH-107, +// AUTH-097 shipped 2026-05-29. +// +// Lives in package handlers so it can use the same in-package +// recordingMailer / setupCoverageRedis fixtures established by +// magic_link_coverage_test.go and cli_auth_coverage_test.go. + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "instant.dev/internal/config" + "instant.dev/internal/middleware" +) + +// csrfRLApp wires the magic-link Start route with an isolated Redis +// connection so the per-IP counter can be observed independently from +// other tests. setupCoverageRedis flushes-on-cleanup via DB-14 isolation. +func csrfRLApp(t *testing.T, rdb *redis.Client) (*fiber.App, *recordingMailer) { + t.Helper() + cfg := &config.Config{JWTSecret: logoutTestSecret} + authH := NewAuthHandler(nil, cfg) + mailer := &recordingMailer{} + h := NewMagicLinkHandlerWithMailerAndRedis(nil, cfg, mailer, authH, rdb) + app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + if errors.Is(err, ErrResponseWritten) { + return nil + } + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{"ok": false, "error": err.Error()}) + }, + }) + app.Use(middleware.RequestID()) + app.Post("/auth/email/start", h.Start) + return app, mailer +} + +// flushPerIPRL clears the per-IP Redis budget so tests in the same +// package don't poison each other. Mirrors the cleanup the per-email +// limit relies on (setupCoverageRedis uses DB 14 which is otherwise +// untouched, but the magicLinkPerIPRLKeyPrefix is new and not auto- +// flushed by any existing helper — so we clear it explicitly). +func flushPerIPRL(t *testing.T, rdb *redis.Client) { + t.Helper() + ctx := context.Background() + iter := rdb.Scan(ctx, 0, magicLinkPerIPRLKeyPrefix+":*", 1000).Iterator() + for iter.Next(ctx) { + _ = rdb.Del(ctx, iter.Val()).Err() + } +} + +// TestAuthEmailStart_RejectsFormUrlencoded — AUTH-163. +// +// Original exploit (QA confirmed): +// +// POST /auth/email/start +// Content-Type: application/x-www-form-urlencoded +// email=qa@x.com +// → HTTP 202 + magic-link inserted in platform_db. +// +// Combined with no Origin enforcement this was a textbook CSRF: any +// malicious site could