package auth import ( "net/http" "net/http/httptest" "sync" "testing" "time" ) func TestSessionIssueReadAndClear(t *testing.T) { manager := NewSessionManager("pulse_test_session", time.Hour, true) now := time.Now() response := httptest.NewRecorder() principal := Principal{Subject: "subject-1", Role: RoleViewer} if err := manager.Issue(response, principal, now); err != nil { t.Fatal(err) } if response.Header().Get("Set-Cookie") == "" { t.Fatal("session cookie was not set") } if !response.Result().Cookies()[0].HttpOnly || !response.Result().Cookies()[0].Secure { t.Fatal("session cookie is not hardened") } request := httptest.NewRequest("GET", "/", nil) for _, cookie := range response.Result().Cookies() { request.AddCookie(cookie) } got, ok := manager.Principal(request, now.Add(time.Minute)) if !ok || got != principal { t.Fatalf("session principal = %#v, %v", got, ok) } clearResponse := httptest.NewRecorder() manager.Clear(clearResponse, request) if _, ok := manager.Principal(request, now.Add(time.Minute)); ok { t.Fatal("cleared session remained valid") } } func TestSessionExpires(t *testing.T) { manager := NewSessionManager("pulse_test_session", time.Minute, false) now := time.Now() response := httptest.NewRecorder() if err := manager.Issue(response, Principal{Subject: "subject-1", Role: RoleViewer}, now); err != nil { t.Fatal(err) } request := httptest.NewRequest("GET", "/", nil) request.AddCookie(response.Result().Cookies()[0]) if _, ok := manager.Principal(request, now.Add(2*time.Minute)); ok { t.Fatal("expired session remained valid") } } func TestSlidingSessionRenewsIdleDeadlineButHonorsAbsoluteExpiry(t *testing.T) { manager := NewSlidingSessionManager("pulse_test_session", time.Minute, 3*time.Minute, true) now := time.Now().UTC().Truncate(time.Second) issued := httptest.NewRecorder() if err := manager.Issue(issued, Principal{Subject: "wallboard", Role: RoleViewer}, now); err != nil { t.Fatal(err) } cookie := issued.Result().Cookies()[0] request := httptest.NewRequest(http.MethodGet, "/api/v1/system/status", nil) request.AddCookie(cookie) beforeWindow := httptest.NewRecorder() if _, ok := manager.Authenticate(beforeWindow, request, now.Add(20*time.Second)); !ok { t.Fatal("active session was rejected before renewal window") } if beforeWindow.Header().Get("Set-Cookie") != "" { t.Fatal("session renewed before entering the bounded renewal window") } for _, offset := range []time.Duration{40 * time.Second, 80 * time.Second, 130 * time.Second} { response := httptest.NewRecorder() if _, ok := manager.Authenticate(response, request, now.Add(offset)); !ok { t.Fatalf("active session was rejected at %s", offset) } renewed := response.Result().Cookies() if len(renewed) != 1 || renewed[0].Value != cookie.Value || renewed[0].Expires.After(now.Add(3*time.Minute)) { t.Fatalf("unsafe renewal at %s: %#v", offset, renewed) } } if _, ok := manager.Authenticate(httptest.NewRecorder(), request, now.Add(3*time.Minute)); ok { t.Fatal("sliding session exceeded its absolute expiry") } } func TestSlidingSessionConcurrentRenewalKeepsTokenUsable(t *testing.T) { manager := NewSlidingSessionManager("pulse_test_session", time.Minute, time.Hour, false) now := time.Now().UTC() issued := httptest.NewRecorder() if err := manager.Issue(issued, Principal{Subject: "wallboard", Role: RoleViewer}, now); err != nil { t.Fatal(err) } cookie := issued.Result().Cookies()[0] const workers = 24 var wait sync.WaitGroup errors := make(chan string, workers) for index := 0; index < workers; index++ { wait.Add(1) go func() { defer wait.Done() request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboards", nil) request.AddCookie(cookie) if _, ok := manager.Authenticate(httptest.NewRecorder(), request, now.Add(40*time.Second)); !ok { errors <- "concurrent renewal rejected a valid token" } }() } wait.Wait() close(errors) for message := range errors { t.Error(message) } request := httptest.NewRequest(http.MethodGet, "/api/v1/dashboards", nil) request.AddCookie(cookie) if _, ok := manager.Principal(request, now.Add(90*time.Second)); !ok { t.Fatal("stable token was invalidated by concurrent renewal") } } func TestSessionAuthenticationContextIsRevokedByClear(t *testing.T) { manager := NewSlidingSessionManager("pulse_test_session", time.Minute, time.Hour, true) now := time.Now().UTC() issued := httptest.NewRecorder() if err := manager.Issue(issued, Principal{Subject: "viewer", Role: RoleViewer}, now); err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/v1/live", nil) request.AddCookie(issued.Result().Cookies()[0]) authentication, ok := manager.AuthenticateSession(httptest.NewRecorder(), request, now.Add(time.Second)) if !ok || authentication.Context == nil { t.Fatal("session authentication context was not returned") } manager.Clear(httptest.NewRecorder(), request) select { case <-authentication.Context.Done(): case <-time.After(time.Second): t.Fatal("cleared session context remained active") } } func TestSessionAuthenticationContextEndsAtAbsoluteExpiry(t *testing.T) { manager := NewSlidingSessionManager("pulse_test_session", 25*time.Millisecond, 25*time.Millisecond, true) now := time.Now().UTC() issued := httptest.NewRecorder() if err := manager.Issue(issued, Principal{Subject: "viewer", Role: RoleViewer}, now); err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/api/v1/live", nil) request.AddCookie(issued.Result().Cookies()[0]) authentication, ok := manager.AuthenticateSession(httptest.NewRecorder(), request, now) if !ok { t.Fatal("new session was rejected") } select { case <-authentication.Context.Done(): case <-time.After(time.Second): t.Fatal("session context exceeded its absolute deadline") } } func TestSessionStoreEvictsOldestSessionsPerSubject(t *testing.T) { manager := NewSlidingSessionManager("pulse_test_session", time.Hour, 24*time.Hour, true) manager.MaxSessionsPerSubject = 3 now := time.Now().UTC() for index := 0; index < 12; index++ { if err := manager.Issue(httptest.NewRecorder(), Principal{Subject: "viewer", Role: RoleViewer}, now.Add(time.Duration(index)*time.Second)); err != nil { t.Fatal(err) } } if got := len(manager.sessions); got != 3 { t.Fatalf("session store size = %d, want 3", got) } } func TestSessionIssuePurgesExpiredEntriesAndHonorsGlobalCapacity(t *testing.T) { manager := NewSessionManager("pulse_test_session", time.Minute, true) manager.MaxSessions = 2 manager.MaxSessionsPerSubject = 2 now := time.Now().UTC() for _, subject := range []string{"viewer-1", "viewer-2"} { if err := manager.Issue(httptest.NewRecorder(), Principal{Subject: subject, Role: RoleViewer}, now); err != nil { t.Fatal(err) } } if err := manager.Issue(httptest.NewRecorder(), Principal{Subject: "viewer-3", Role: RoleViewer}, now); err == nil { t.Fatal("session capacity was not enforced") } if err := manager.Issue(httptest.NewRecorder(), Principal{Subject: "viewer-3", Role: RoleViewer}, now.Add(2*time.Minute)); err != nil { t.Fatalf("expired sessions were not purged: %v", err) } if got := len(manager.sessions); got != 1 { t.Fatalf("session store size after purge = %d, want 1", got) } }