package auth import ( "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestManagerRejectsTamperedAndExpiredCookies(t *testing.T) { manager := NewManager([]byte(strings.Repeat("s", 32)), true) request := httptest.NewRequest(http.MethodGet, "/login", nil) response := httptest.NewRecorder() csrf, authenticated := manager.Ensure(response, request) if csrf == "" || authenticated { t.Fatalf("Ensure = (%q, %t), want anonymous CSRF session", csrf, authenticated) } cookie := response.Result().Cookies()[0] if !cookie.HttpOnly || !cookie.Secure || cookie.SameSite != http.SameSiteLaxMode || cookie.Path != "/" { t.Fatalf("session cookie is missing security attributes: %#v", cookie) } tampered := *cookie tampered.Value = flipCookieValue(t, cookie.Value) tamperedRequest := httptest.NewRequest(http.MethodPost, "/login", nil) tamperedRequest.AddCookie(&tampered) if _, ok := manager.VerifyCSRF(tamperedRequest, csrf); ok { t.Fatal("tampered signed cookie passed CSRF verification") } manager.now = func() time.Time { return time.Now().Add(9 * time.Hour) } expiredRequest := httptest.NewRequest(http.MethodPost, "/login", nil) expiredRequest.AddCookie(cookie) if _, ok := manager.VerifyCSRF(expiredRequest, csrf); ok { t.Fatal("expired cookie passed CSRF verification") } } func TestIsAuthenticatedDoesNotCreateOrDependOnCSRFValidation(t *testing.T) { manager := NewManager([]byte(strings.Repeat("s", 32)), false) missingSession := httptest.NewRequest(http.MethodPost, "/tasks/start-purchases", nil) if manager.IsAuthenticated(missingSession) { t.Fatal("missing session was treated as authenticated") } if len(manager.sessions) != 0 { t.Fatalf("read-only authentication check created %d sessions", len(manager.sessions)) } anonymousRequest := httptest.NewRequest(http.MethodGet, "/login", nil) anonymousResponse := httptest.NewRecorder() manager.Ensure(anonymousResponse, anonymousRequest) anonymousCookie := anonymousResponse.Result().Cookies()[0] anonymousCheck := httptest.NewRequest(http.MethodPost, "/tasks/start-purchases", nil) anonymousCheck.AddCookie(anonymousCookie) if manager.IsAuthenticated(anonymousCheck) { t.Fatal("anonymous CSRF session was treated as authenticated") } loginRequest := httptest.NewRequest(http.MethodPost, "/login", nil) loginRequest.AddCookie(anonymousCookie) authenticatedResponse := httptest.NewRecorder() csrf := manager.RotateAuthenticated(authenticatedResponse, loginRequest) authenticatedCookie := authenticatedResponse.Result().Cookies()[0] authenticatedCheck := httptest.NewRequest(http.MethodPost, "/tasks/start-purchases", nil) authenticatedCheck.AddCookie(authenticatedCookie) if !manager.IsAuthenticated(authenticatedCheck) { t.Fatal("valid authenticated session was not recognized") } if authenticated, csrfOK := manager.VerifyCSRF(authenticatedCheck, "wrong-token"); authenticated || csrfOK { t.Fatalf("wrong token result = (%t, %t), want (false, false)", authenticated, csrfOK) } validRequest := httptest.NewRequest(http.MethodPost, "/tasks/start-purchases", nil) validRequest.AddCookie(authenticatedCookie) if authenticated, csrfOK := manager.VerifyCSRF(validRequest, csrf); !authenticated || !csrfOK { t.Fatalf("valid token result = (%t, %t), want (true, true)", authenticated, csrfOK) } if authenticated, csrfOK := manager.VerifyCSRF(httptest.NewRequest(http.MethodPost, "/tasks/start-purchases", nil), csrf); authenticated || csrfOK { t.Fatalf("missing session result = (%t, %t), want (false, false)", authenticated, csrfOK) } } func flipCookieValue(t *testing.T, value string) string { t.Helper() if value == "" { t.Fatal("cannot tamper with an empty cookie") } if value[0] == 'A' { return "B" + value[1:] } return "A" + value[1:] }