check / check (push) Canceled after 0s
seal.DeriveKeys derives the session hash and block keys, the encrypted URL key and the CSRF key from the signing key, with their salts declared beside it, and returns them as one seal.Keys. internal/providers calls it once at startup and provides the value to the fx graph. The session manager, the encrypted URL generator and the CSRF protection each take their own key. session.NewManager, encurl.NewGenerator, newCSRFProtect and handlers.New return no error, as nothing in them can fail now. Each key is derived as before, with seal.DeriveKey and the same salt. The signing key itself stays the HMAC key of signed URLs and the login password. README.md says under signing_key what it protects. Model: opus-5-5
251 lines
5.7 KiB
Go
251 lines
5.7 KiB
Go
package session_test
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
func TestManager_CreateAndValidate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, err := seal.DeriveKeys("test-signing-key-12345")
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
// Create a session
|
|
w := httptest.NewRecorder()
|
|
|
|
err = mgr.CreateSession(w)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession() error = %v", err)
|
|
}
|
|
|
|
// Extract the cookie from response
|
|
resp := w.Result()
|
|
|
|
cookies := resp.Cookies()
|
|
if len(cookies) == 0 {
|
|
t.Fatal("CreateSession() did not set a cookie")
|
|
}
|
|
|
|
var sessionCookie *http.Cookie
|
|
|
|
for _, c := range cookies {
|
|
if c.Name == session.CookieName {
|
|
sessionCookie = c
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if sessionCookie == nil {
|
|
t.Fatalf("CreateSession() did not set cookie named %q", session.CookieName)
|
|
}
|
|
|
|
// Validate the session
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(sessionCookie)
|
|
|
|
data, err := mgr.ValidateSession(req)
|
|
if err != nil {
|
|
t.Fatalf("ValidateSession() error = %v", err)
|
|
}
|
|
|
|
if !data.Authenticated {
|
|
t.Error("ValidateSession() returned unauthenticated session")
|
|
}
|
|
|
|
if data.ExpiresAt.Before(time.Now()) {
|
|
t.Error("ValidateSession() returned already-expired session")
|
|
}
|
|
}
|
|
|
|
func TestManager_ValidateSession_NoCookie(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
|
|
_, err := mgr.ValidateSession(req)
|
|
if err == nil {
|
|
t.Error("ValidateSession() should fail with no cookie")
|
|
}
|
|
|
|
if !errors.Is(err, session.ErrNoSession) {
|
|
t.Errorf("ValidateSession() error = %v, want %v", err, session.ErrNoSession)
|
|
}
|
|
}
|
|
|
|
func TestManager_ValidateSession_TamperedCookie(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(&http.Cookie{
|
|
Name: session.CookieName,
|
|
Value: "tampered-invalid-cookie-value",
|
|
Secure: true,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
|
|
_, err := mgr.ValidateSession(req)
|
|
if err == nil {
|
|
t.Error("ValidateSession() should fail with tampered cookie")
|
|
}
|
|
|
|
if !errors.Is(err, session.ErrInvalidSession) {
|
|
t.Errorf("ValidateSession() error = %v, want %v", err, session.ErrInvalidSession)
|
|
}
|
|
}
|
|
|
|
func TestManager_ValidateSession_WrongKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys1, _ := seal.DeriveKeys("signing-key-1")
|
|
keys2, _ := seal.DeriveKeys("signing-key-2")
|
|
mgr1 := session.NewManager(keys1.SessionHashKey, keys1.SessionBlockKey)
|
|
mgr2 := session.NewManager(keys2.SessionHashKey, keys2.SessionBlockKey)
|
|
|
|
// Create session with mgr1
|
|
w := httptest.NewRecorder()
|
|
_ = mgr1.CreateSession(w)
|
|
|
|
resp := w.Result()
|
|
|
|
var sessionCookie *http.Cookie
|
|
|
|
for _, c := range resp.Cookies() {
|
|
if c.Name == session.CookieName {
|
|
sessionCookie = c
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Try to validate with mgr2 (different key)
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(sessionCookie)
|
|
|
|
_, err := mgr2.ValidateSession(req)
|
|
if err == nil {
|
|
t.Error("ValidateSession() should fail with different signing key")
|
|
}
|
|
}
|
|
|
|
func TestManager_ClearSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
w := httptest.NewRecorder()
|
|
mgr.ClearSession(w)
|
|
|
|
resp := w.Result()
|
|
cookies := resp.Cookies()
|
|
|
|
var sessionCookie *http.Cookie
|
|
|
|
for _, c := range cookies {
|
|
if c.Name == session.CookieName {
|
|
sessionCookie = c
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if sessionCookie == nil {
|
|
t.Fatal("ClearSession() did not set a cookie")
|
|
}
|
|
|
|
if sessionCookie.MaxAge != -1 {
|
|
t.Errorf("ClearSession() cookie MaxAge = %d, want -1", sessionCookie.MaxAge)
|
|
}
|
|
}
|
|
|
|
func TestManager_IsAuthenticated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-signing-key-12345")
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
// No session - should return false
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
if mgr.IsAuthenticated(req) {
|
|
t.Error("IsAuthenticated() should return false with no session")
|
|
}
|
|
|
|
// Create session
|
|
w := httptest.NewRecorder()
|
|
_ = mgr.CreateSession(w)
|
|
|
|
resp := w.Result()
|
|
|
|
var sessionCookie *http.Cookie
|
|
|
|
for _, c := range resp.Cookies() {
|
|
if c.Name == session.CookieName {
|
|
sessionCookie = c
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// With valid session - should return true
|
|
req = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(sessionCookie)
|
|
|
|
if !mgr.IsAuthenticated(req) {
|
|
t.Error("IsAuthenticated() should return true with valid session")
|
|
}
|
|
}
|
|
|
|
func TestManager_CookieAttributes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, _ := seal.DeriveKeys("test-key")
|
|
mgr := session.NewManager(keys.SessionHashKey, keys.SessionBlockKey)
|
|
|
|
w := httptest.NewRecorder()
|
|
_ = mgr.CreateSession(w)
|
|
|
|
resp := w.Result()
|
|
|
|
var sessionCookie *http.Cookie
|
|
|
|
for _, c := range resp.Cookies() {
|
|
if c.Name == session.CookieName {
|
|
sessionCookie = c
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !sessionCookie.HttpOnly {
|
|
t.Error("Cookie should have HttpOnly flag")
|
|
}
|
|
|
|
if !sessionCookie.Secure {
|
|
t.Error("Cookie should have Secure flag when manager created with secure=true")
|
|
}
|
|
|
|
if sessionCookie.SameSite != http.SameSiteStrictMode {
|
|
t.Errorf("Cookie SameSite = %v, want %v",
|
|
sessionCookie.SameSite, http.SameSiteStrictMode)
|
|
}
|
|
}
|