Files
member-console/internal/auth/auth_session_rotation_test.go
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

95 lines
3.5 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package auth
import (
"net/http"
"net/http/httptest"
"testing"
"golang.org/x/oauth2"
)
// tokenAfter runs h through the session manager for one request and returns
// the session token the response sets, or "" when the token was not rotated.
func tokenAfter(t *testing.T, cfg *Config, h http.HandlerFunc, req *http.Request) string {
t.Helper()
rec := httptest.NewRecorder()
cfg.SessionManager.LoadAndSave(h).ServeHTTP(rec, req)
for _, c := range rec.Result().Cookies() {
if c.Name == cfg.SessionManager.Cookie.Name {
return c.Value
}
}
return ""
}
// Session fixation, found by the 2026-09 security audit: LoginHandler rotated
// the session token before starting the OIDC flow but RegistrationHandler did
// not, so a session id planted in a victim's browser survived their sign-in
// through /register.
//
// The handler is exercised only as far as the rotation, which happens before
// any OIDC or Viper wiring is touched; the request is expected to fail later
// (there is no configured issuer here) and that is fine — what is asserted is
// that a new session token was issued regardless.
func TestRegistrationHandlerRotatesTheSessionToken(t *testing.T) {
cfg := newTestConfig()
// Establish a session and capture its token, standing in for the id an
// attacker would have planted.
seed := httptest.NewRequest(http.MethodGet, "/", nil)
planted := tokenAfter(t, cfg, func(w http.ResponseWriter, r *http.Request) {
cfg.SessionManager.Put(r.Context(), "seeded", true)
}, seed)
if planted == "" {
t.Fatal("setup: no session token was issued for the seeding request")
}
// Replay that session id into the registration path.
req := httptest.NewRequest(http.MethodGet, "/register", nil)
req.AddCookie(&http.Cookie{Name: cfg.SessionManager.Cookie.Name, Value: planted})
rotated := tokenAfter(t, cfg, cfg.RegistrationHandler, req)
if rotated == "" {
t.Fatal("RegistrationHandler issued no new session token: the planted id survives")
}
if rotated == planted {
t.Errorf("session token was not rotated (%q); a fixed session id survives registration", rotated)
}
}
// The login path carries the same guarantee, so a regression there is caught
// too. LoginHandler needs OAuthConfig only for AuthCodeURL, which builds a
// string and talks to nothing, so a dummy config is enough — no live identity
// provider, no network.
func TestLoginHandlerRotatesTheSessionToken(t *testing.T) {
cfg := newTestConfig()
cfg.OAuthConfig = &oauth2.Config{
ClientID: "test-client",
RedirectURL: "http://example.test/callback",
Endpoint: oauth2.Endpoint{AuthURL: "http://idp.test/auth", TokenURL: "http://idp.test/token"},
Scopes: []string{"openid"},
}
seed := httptest.NewRequest(http.MethodGet, "/", nil)
planted := tokenAfter(t, cfg, func(w http.ResponseWriter, r *http.Request) {
cfg.SessionManager.Put(r.Context(), "seeded", true)
}, seed)
if planted == "" {
t.Fatal("setup: no session token was issued for the seeding request")
}
req := httptest.NewRequest(http.MethodGet, "/login", nil)
req.AddCookie(&http.Cookie{Name: cfg.SessionManager.Cookie.Name, Value: planted})
rotated := tokenAfter(t, cfg, cfg.LoginHandler, req)
if rotated == "" {
t.Fatal("LoginHandler issued no new session token: the planted id survives")
}
if rotated == planted {
t.Errorf("session token was not rotated (%q); a fixed session id survives sign-in", rotated)
}
}