feat(auth): gleitende Sitzung – Frist ab letzter Benutzung statt ab Anmeldung
Marc flog trotz täglicher Nutzung heraus: Die Frist lief ab dem
Anmelden. Ein Ablauf, der den Anwesenden trifft und nicht den
Abwesenden, erkauft keine Sicherheit, sondern nur Ärger.
Jetzt verlängert jeder angemeldete Aufruf die Sitzung – höchstens einmal
pro Stunde, sonst trüge jede Antwort ein Set-Cookie (beim Laden einer
Dateiliste dreißig Stück). Vorgabe 30 Tage ab letzter Benutzung;
bestehende .env-Werte bleiben unangetastet.
security(auth): Frist galt nur im Browser, nicht für die Signatur
Beim Bauen gefunden, war schon vorher drin: NewSessionStore ersetzte
store.Options, ohne store.MaxAge() zu rufen. Die Codecs behielten damit
gorillas Voreinstellung von 30 Tagen. Wirkung: Der Browser warf das
Cookie nach SESSION_MAX_AGE weg – wer den Wert vorher herauskopiert
hatte, konnte ihn 30 Tage lang weiterbenutzen. Wer die Frist kürzte, um
sie zu kürzen, hatte nichts gekürzt.
Mit Test festgehalten, und der Test ist gegengeprüft: Ohne den Aufruf
schlägt er fehl ("abgelaufene Sitzung gilt weiter"), mit ihm läuft er
durch.
Ein Fehler steckte dabei zuerst im Test selbst: gorilla legt die
entschlüsselte Sitzung im Zusammenhang der Anfrage ab. Zweimal dieselbe
Anfrage zu befragen prüfte den Zwischenspeicher, nicht die Frist. Jetzt
baut jede Prüfung eine frische Anfrage.
Live gegengeprüft: Anmeldung setzt das Cookie, drei angemeldete Aufrufe
danach setzen keins (die Bremse greift).
Dazu die gemessenen Leerlaufwerte der Office-Server im README statt der
Zahlen aus deren Dokumentation (Collabora 51 MB, OnlyOffice 396 MB).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6c53651568
commit
093ff5140e
6 files changed
+227
-6
No files matched your search
@@ -0,0 +1,125 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const testSecret = "test-geheimnis-mit-mehr-als-32-zeichen!!"
|
||||
|
||||
// Anmelden und die gesetzten Cookies zurückgeben.
|
||||
func signIn(t *testing.T, ss *SessionStore) []*http.Cookie {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if err := ss.SetUser(w, r, uuid.New()); err != nil {
|
||||
t.Fatalf("SetUser: %v", err)
|
||||
}
|
||||
return w.Result().Cookies()
|
||||
}
|
||||
|
||||
// Eine **frische** Anfrage mit diesen Cookies.
|
||||
//
|
||||
// Jedes Mal eine neue, und das ist keine Umständlichkeit: gorilla legt die
|
||||
// entschlüsselte Sitzung im Zusammenhang der Anfrage ab und gibt sie beim
|
||||
// zweiten Zugriff unverändert zurück. Zweimal dieselbe Anfrage zu befragen
|
||||
// prüfte also den Zwischenspeicher und nicht die Frist – der erste Entwurf
|
||||
// dieses Tests ist genau darauf hereingefallen.
|
||||
func withCookies(cookies []*http.Cookie) *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, c := range cookies {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func signedIn(t *testing.T, ss *SessionStore) *http.Request {
|
||||
t.Helper()
|
||||
return withCookies(signIn(t, ss))
|
||||
}
|
||||
|
||||
// Die Frist muss auch für den **Inhalt** gelten, nicht nur für das Cookie.
|
||||
//
|
||||
// Der Browser wirft ein abgelaufenes Cookie von selbst weg – wer es vorher
|
||||
// herauskopiert hat, aber nicht. Ohne store.MaxAge() blieb die Signatur 30
|
||||
// Tage gültig, egal was in SESSION_MAX_AGE stand.
|
||||
func TestSessionExpiresAfterMaxAge(t *testing.T) {
|
||||
ss := NewSessionStore(testSecret, 1) // eine Sekunde
|
||||
cookies := signIn(t, ss)
|
||||
|
||||
if _, ok := ss.GetUserID(withCookies(cookies)); !ok {
|
||||
t.Fatal("frische Sitzung wurde nicht erkannt")
|
||||
}
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
if _, ok := ss.GetUserID(withCookies(cookies)); ok {
|
||||
t.Error("abgelaufene Sitzung gilt weiter – die Frist wirkt nur im Browser")
|
||||
}
|
||||
}
|
||||
|
||||
// Nicht bei jedem Aufruf ein neues Cookie: Sonst trüge jede Antwort ein
|
||||
// Set-Cookie, und beim Laden einer Dateiliste sind das dreißig Stück.
|
||||
func TestTouchIsThrottled(t *testing.T) {
|
||||
ss := NewSessionStore(testSecret, 3600)
|
||||
r := signedIn(t, ss)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ss.Touch(w, r)
|
||||
if len(w.Result().Cookies()) != 0 {
|
||||
t.Error("Sitzung wurde sofort erneuert, obwohl sie eben erst entstand")
|
||||
}
|
||||
}
|
||||
|
||||
// Nach Ablauf der Bremse verlängert sie sich – das ist der eigentliche Zweck.
|
||||
func TestTouchExtendsSession(t *testing.T) {
|
||||
old := refreshInterval
|
||||
refreshInterval = 0
|
||||
defer func() { refreshInterval = old }()
|
||||
|
||||
ss := NewSessionStore(testSecret, 3600)
|
||||
r := signedIn(t, ss)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ss.Touch(w, r)
|
||||
cookies := w.Result().Cookies()
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("Sitzung wurde nicht verlängert")
|
||||
}
|
||||
// Das erneuerte Cookie muss dieselbe Anmeldung tragen.
|
||||
next := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, c := range cookies {
|
||||
next.AddCookie(c)
|
||||
}
|
||||
if _, ok := ss.GetUserID(next); !ok {
|
||||
t.Error("nach dem Verlängern ist niemand mehr angemeldet")
|
||||
}
|
||||
}
|
||||
|
||||
// Eine Anfrage ohne Cookie darf keine Sitzung erfinden.
|
||||
func TestTouchIgnoresAnonymous(t *testing.T) {
|
||||
ss := NewSessionStore(testSecret, 3600)
|
||||
w := httptest.NewRecorder()
|
||||
ss.Touch(w, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
if len(w.Result().Cookies()) != 0 {
|
||||
t.Error("für einen Unangemeldeten wurde ein Cookie gesetzt")
|
||||
}
|
||||
}
|
||||
|
||||
// Abmelden muss stärker sein als Verlängern.
|
||||
func TestClearEndsSession(t *testing.T) {
|
||||
ss := NewSessionStore(testSecret, 3600)
|
||||
r := signedIn(t, ss)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
if err := ss.Clear(w, r); err != nil {
|
||||
t.Fatalf("Clear: %v", err)
|
||||
}
|
||||
for _, c := range w.Result().Cookies() {
|
||||
if c.Name == sessionName && c.MaxAge >= 0 {
|
||||
t.Errorf("Abmelden löscht das Cookie nicht (MaxAge=%d)", c.MaxAge)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user