diff options
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/handlers.go | 4 | ||||
| -rw-r--r-- | internal/api/handlers_test.go | 91 |
2 files changed, 93 insertions, 2 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 46ab41e..8b207c6 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -222,7 +222,7 @@ func (s *Server) setSessionCookie(w http.ResponseWriter, value string) { Value: value, Path: "/", HttpOnly: true, - Secure: true, + Secure: s.cfg.SecureCookies, SameSite: http.SameSiteStrictMode, Expires: time.Now().Add(time.Duration(s.cfg.SessionTimeoutHours) * time.Hour), }) @@ -234,7 +234,7 @@ func (s *Server) clearSessionCookie(w http.ResponseWriter) { Value: "", Path: "/", HttpOnly: true, - Secure: true, + Secure: s.cfg.SecureCookies, SameSite: http.SameSiteStrictMode, MaxAge: -1, Expires: time.Unix(0, 0), diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 8c2161c..99c009b 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -458,6 +458,97 @@ func TestServer_Login(t *testing.T) { }) } +func TestServer_SessionCookieSecure(t *testing.T) { + hasher := &staticHasher{fixed: "hashed"} + store := &repository.MockStore{ + UserRepo: repository.MockUserRepo{ + CountUsersFunc: func(ctx context.Context) (int, error) { return 1, nil }, + GetUserByUsernameFunc: func(ctx context.Context, username string) (*model.User, error) { + return &model.User{ID: 1, Username: "alice", PasswordHash: "hashed"}, nil + }, + }, + } + repo := repository.MockSessionRepo{ + CreateSessionFunc: func(ctx context.Context, session *model.Session) error { return nil }, + } + sm := auth.NewSessionManager(&repo, &clock.MockClock{T: time.Now()}, time.Hour) + + t.Run("Secure=true by default", func(t *testing.T) { + cfg := &internal.Config{SessionTimeoutHours: 24, SecureCookies: true} + srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, nil) + body := `{"username":"alice","password":"correct"}` + req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code) + } + for _, c := range rr.Result().Cookies() { + if c.Name == "session" && c.Secure != true { + t.Fatalf("expected Secure=true, got Secure=%v", c.Secure) + } + } + }) + + t.Run("Secure=false", func(t *testing.T) { + cfg := &internal.Config{SessionTimeoutHours: 24, SecureCookies: false} + srv := newTestServer(t, store, hasher, sm, cfg, nil, nil, nil, nil) + body := `{"username":"alice","password":"correct"}` + req := httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("expected %d, got %d", http.StatusOK, rr.Code) + } + for _, c := range rr.Result().Cookies() { + if c.Name == "session" && c.Secure != false { + t.Fatalf("expected Secure=false, got Secure=%v", c.Secure) + } + } + }) + + t.Run("clear cookie respects Secure config", func(t *testing.T) { + var deleted string + sessStore := &repository.MockStore{ + UserRepo: repository.MockUserRepo{ + CountUsersFunc: func(ctx context.Context) (int, error) { return 1, nil }, + }, + SessionRepo: repository.MockSessionRepo{ + GetSessionByIDFunc: func(ctx context.Context, id string) (*model.Session, error) { + if id == "abc" { + return &model.Session{ID: "abc", UserID: 1, ExpiresAt: time.Now().Add(time.Hour)}, nil + } + return nil, nil + }, + DeleteSessionFunc: func(ctx context.Context, id string) error { + deleted = id + return nil + }, + }, + } + logoutSM := auth.NewSessionManager(&sessStore.SessionRepo, &clock.MockClock{T: time.Now()}, time.Hour) + cfg := &internal.Config{SessionTimeoutHours: 24, SecureCookies: false} + srv := newTestServer(t, sessStore, nil, logoutSM, cfg, nil, nil, nil, nil) + req := httptest.NewRequest(http.MethodPost, "/api/logout", nil) + req.AddCookie(&http.Cookie{Name: "session", Value: "abc"}) + rr := httptest.NewRecorder() + srv.ServeHTTP(rr, req) + if rr.Code != http.StatusNoContent { + t.Fatalf("expected %d, got %d", http.StatusNoContent, rr.Code) + } + if deleted != "abc" { + t.Fatalf("expected session abc to be deleted, got %q", deleted) + } + for _, c := range rr.Result().Cookies() { + if c.Name == "session" && c.Secure != false { + t.Fatalf("expected Secure=false on cleared cookie, got Secure=%v", c.Secure) + } + } + }) +} + func TestServer_Logout(t *testing.T) { cfg := &internal.Config{SessionTimeoutHours: 24} |
