From 839febb83a884464e7e2eebf15ef4e74f952cfe4 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Wed, 1 Jul 2026 18:24:35 +0700 Subject: [PATCH] fix(httpapi): bind session token to current AuthUser; add negative auth tests Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd --- internal/httpapi/auth.go | 3 ++- internal/httpapi/auth_test.go | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/httpapi/auth.go b/internal/httpapi/auth.go index 6de0309..378e3e7 100644 --- a/internal/httpapi/auth.go +++ b/internal/httpapi/auth.go @@ -58,7 +58,8 @@ func (s *Server) requireAuth(next http.Handler) http.Handler { http.Error(w, "unauthorized", http.StatusUnauthorized) return } - if _, ok := crypto.VerifySession(s.cfg.SessionSecret, c.Value, time.Now()); !ok { + user, ok := crypto.VerifySession(s.cfg.SessionSecret, c.Value, time.Now()) + if !ok || user != s.cfg.AuthUser { http.Error(w, "unauthorized", http.StatusUnauthorized) return } diff --git a/internal/httpapi/auth_test.go b/internal/httpapi/auth_test.go index 71b9c82..269c7f8 100644 --- a/internal/httpapi/auth_test.go +++ b/internal/httpapi/auth_test.go @@ -5,8 +5,10 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/vasyansk/imap-copier/internal/config" + "github.com/vasyansk/imap-copier/internal/crypto" ) func testServer() *Server { @@ -55,3 +57,39 @@ func TestRequireAuthAllowsValidCookie(t *testing.T) { t.Fatalf("want 200, got %d", rw.Code) } } + +func TestLoginRejectsBadCredentials(t *testing.T) { + s := testServer() + req := httptest.NewRequest("POST", "/api/login", strings.NewReader(`{"user":"admin","pass":"wrong"}`)) + rw := httptest.NewRecorder() + s.handleLogin(rw, req) + if rw.Code != http.StatusUnauthorized { + t.Fatalf("want 401, got %d", rw.Code) + } +} + +func TestRequireAuthRejectsTamperedCookie(t *testing.T) { + s := testServer() + h := s.requireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })) + req := httptest.NewRequest("GET", "/api/tasks", nil) + req.AddCookie(&http.Cookie{Name: cookieName, Value: "not.a.validtoken"}) + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + if rw.Code != http.StatusUnauthorized { + t.Fatalf("want 401, got %d", rw.Code) + } +} + +func TestRequireAuthRejectsTokenForDifferentUser(t *testing.T) { + s := testServer() + // token signed for a user that is NOT s.cfg.AuthUser ("admin") + tok := crypto.SignSession(s.cfg.SessionSecret, "olduser", time.Now().Add(time.Hour)) + h := s.requireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })) + req := httptest.NewRequest("GET", "/api/tasks", nil) + req.AddCookie(&http.Cookie{Name: cookieName, Value: tok}) + rw := httptest.NewRecorder() + h.ServeHTTP(rw, req) + if rw.Code != http.StatusUnauthorized { + t.Fatalf("stale-user token must be rejected, got %d", rw.Code) + } +}