check / check (push) Waiting to run
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
160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
package session_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/database"
|
|
"sneak.berlin/go/pixa/internal/seal"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
const logoutTestSigningKey = "test-signing-key-12345"
|
|
|
|
// newTestDatabase opens an empty in-memory database with pixa's schema. It
|
|
// has one connection, as pixad's has: each connection to ":memory:" would
|
|
// open a database of its own.
|
|
func newTestDatabase(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("sql.Open() error = %v", err)
|
|
}
|
|
|
|
db.SetMaxOpenConns(1)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
err = database.ApplyMigrations(t.Context(), db, nil)
|
|
if err != nil {
|
|
t.Fatalf("ApplyMigrations() error = %v", err)
|
|
}
|
|
|
|
return db
|
|
}
|
|
|
|
// createSessionCookie logs in with mgr and returns the session cookie it
|
|
// sets.
|
|
func createSessionCookie(t *testing.T, mgr *session.Manager) *http.Cookie {
|
|
t.Helper()
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
err := mgr.CreateSession(rec)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession() error = %v", err)
|
|
}
|
|
|
|
for _, c := range rec.Result().Cookies() {
|
|
if c.Name == session.CookieName {
|
|
return c
|
|
}
|
|
}
|
|
|
|
t.Fatalf("CreateSession() did not set a %q cookie", session.CookieName)
|
|
|
|
return nil
|
|
}
|
|
|
|
// requestWithCookie returns a GET request carrying cookie, or no cookie when
|
|
// cookie is nil.
|
|
func requestWithCookie(t *testing.T, cookie *http.Cookie) *http.Request {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
if cookie != nil {
|
|
req.AddCookie(cookie)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// TestLogout_EndsEverySessionMadeBeforeIt verifies that after a logout a
|
|
// copy of the session cookie taken before it is refused and a login after it
|
|
// works, and that both still hold for a new Manager on the same database, as
|
|
// after a restart.
|
|
func TestLogout_EndsEverySessionMadeBeforeIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := newTestDatabase(t)
|
|
|
|
keys, err := seal.DeriveKeys(logoutTestSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
mgr, err := session.NewManagerWithDatabase(
|
|
t.Context(), keys.SessionHashKey, keys.SessionBlockKey, db)
|
|
if err != nil {
|
|
t.Fatalf("NewManagerWithDatabase() error = %v", err)
|
|
}
|
|
|
|
copied := createSessionCookie(t, mgr)
|
|
|
|
err = mgr.Logout(httptest.NewRecorder(), requestWithCookie(t, copied))
|
|
if err != nil {
|
|
t.Fatalf("Logout() error = %v", err)
|
|
}
|
|
|
|
_, err = mgr.ValidateSession(requestWithCookie(t, copied))
|
|
t.Logf("cookie copied before the logout: ValidateSession() error = %v", err)
|
|
|
|
if !errors.Is(err, session.ErrInvalidSession) {
|
|
t.Errorf("cookie copied before the logout: error = %v, want %v",
|
|
err, session.ErrInvalidSession)
|
|
}
|
|
|
|
loginAfter := createSessionCookie(t, mgr)
|
|
|
|
_, err = mgr.ValidateSession(requestWithCookie(t, loginAfter))
|
|
if err != nil {
|
|
t.Errorf("login after the logout: ValidateSession() error = %v", err)
|
|
}
|
|
|
|
restarted, err := session.NewManagerWithDatabase(
|
|
t.Context(), keys.SessionHashKey, keys.SessionBlockKey, db)
|
|
if err != nil {
|
|
t.Fatalf("NewManagerWithDatabase() after restart error = %v", err)
|
|
}
|
|
|
|
if restarted.IsAuthenticated(requestWithCookie(t, copied)) {
|
|
t.Error("after a restart, a cookie copied before the logout is accepted")
|
|
}
|
|
|
|
if !restarted.IsAuthenticated(requestWithCookie(t, loginAfter)) {
|
|
t.Error("after a restart, a login made after the logout is refused")
|
|
}
|
|
}
|
|
|
|
// TestLogout_WithoutSession_EndsNoSession verifies that a logout from a
|
|
// request without a session cookie ends no session, so someone who has not
|
|
// logged in cannot end the sessions of those who have.
|
|
func TestLogout_WithoutSession_EndsNoSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keys, err := seal.DeriveKeys(logoutTestSigningKey)
|
|
if err != nil {
|
|
t.Fatalf("seal.DeriveKeys() error = %v", err)
|
|
}
|
|
|
|
mgr, err := session.NewManagerWithDatabase(t.Context(),
|
|
keys.SessionHashKey, keys.SessionBlockKey, newTestDatabase(t))
|
|
if err != nil {
|
|
t.Fatalf("NewManagerWithDatabase() error = %v", err)
|
|
}
|
|
|
|
cookie := createSessionCookie(t, mgr)
|
|
|
|
err = mgr.Logout(httptest.NewRecorder(), requestWithCookie(t, nil))
|
|
if err != nil {
|
|
t.Fatalf("Logout() error = %v", err)
|
|
}
|
|
|
|
if !mgr.IsAuthenticated(requestWithCookie(t, cookie)) {
|
|
t.Error("a logout without a session cookie ended a session")
|
|
}
|
|
}
|