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
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/sessions"
|
||||
@@ -15,10 +16,22 @@ import (
|
||||
const (
|
||||
sessionName = "aether_session"
|
||||
sessionKeyUser = "user_id"
|
||||
csrfCookieName = "aether_csrf"
|
||||
csrfHeaderName = "X-CSRF-Token"
|
||||
// sessionKeyTouched hält fest, wann die Sitzung zuletzt verlängert wurde.
|
||||
// Ohne diesen Wert müsste jede einzelne Anfrage ein neues Cookie setzen.
|
||||
sessionKeyTouched = "touched_at"
|
||||
csrfCookieName = "aether_csrf"
|
||||
csrfHeaderName = "X-CSRF-Token"
|
||||
)
|
||||
|
||||
// refreshInterval: Wie oft eine benutzte Sitzung höchstens erneuert wird.
|
||||
//
|
||||
// Die Frist gleitet – aber sie muss nicht bei jedem Klick nachgezogen werden.
|
||||
// Ohne diese Bremse trüge **jede** Antwort ein `Set-Cookie`, und beim Laden
|
||||
// einer Dateiliste sind das schnell dreißig Stück. Einmal pro Stunde reicht:
|
||||
// Der Unterschied zur Frist ist eine Stunde, und die fällt bei Tagen nicht
|
||||
// ins Gewicht.
|
||||
var refreshInterval = time.Hour
|
||||
|
||||
type SessionStore struct {
|
||||
store *sessions.CookieStore
|
||||
maxAge int
|
||||
@@ -32,6 +45,15 @@ func NewSessionStore(secret string, maxAge int) *SessionStore {
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
// **Zwei Fristen, nicht eine.** `Options.MaxAge` steht nur im Cookie – der
|
||||
// Browser wirft es dann weg. Wie lange der *Inhalt* gilt, entscheidet die
|
||||
// Signatur, und die hängt an den Codecs.
|
||||
//
|
||||
// Ohne diesen Aufruf blieb dort gorillas Voreinstellung von 30 Tagen
|
||||
// stehen: Ein Cookie, das jemand aus einem Browser kopiert hat, wäre einen
|
||||
// Monat lang gültig geblieben, obwohl SESSION_MAX_AGE auf einen Tag stand.
|
||||
// Wer die Frist kürzt, um sie zu kürzen, hätte nichts gekürzt.
|
||||
store.MaxAge(maxAge)
|
||||
return &SessionStore{store: store, maxAge: maxAge}
|
||||
}
|
||||
|
||||
@@ -41,10 +63,45 @@ func (ss *SessionStore) SetUser(w http.ResponseWriter, r *http.Request, userID u
|
||||
sess, _ = ss.store.New(r, sessionName)
|
||||
}
|
||||
sess.Values[sessionKeyUser] = userID.String()
|
||||
sess.Values[sessionKeyTouched] = time.Now().Unix()
|
||||
sess.Options.Secure = middleware.IsHTTPS(r)
|
||||
return ss.store.Save(r, w, sess)
|
||||
}
|
||||
|
||||
// Touch verlängert eine benutzte Sitzung.
|
||||
//
|
||||
// **Warum überhaupt:** Bisher lief die Frist ab dem *Anmelden* – wer die Cloud
|
||||
// täglich benutzte, flog nach 24 Stunden trotzdem heraus, mitten in der
|
||||
// Arbeit. Das ist die Sorte Ablauf, die keine Sicherheit bringt: Sie trifft
|
||||
// den, der da ist, und nicht den, der weg ist.
|
||||
//
|
||||
// Jetzt läuft sie ab der *letzten Benutzung*. Wer die Cloud regelmäßig
|
||||
// öffnet, bleibt angemeldet; wer sie einen Monat nicht anfasst, muss sich neu
|
||||
// anmelden – und genau dieser zweite Fall ist der, um den es bei einem
|
||||
// abgelaufenen Cookie geht.
|
||||
//
|
||||
// **Was das kostet, ehrlich gesagt:** Ein entwendetes Cookie, das weiter
|
||||
// benutzt wird, verlängert sich mit. Ein Sitzungscookie hat keine Liste auf
|
||||
// dem Server, die man widerrufen könnte – das ist der Preis dafür, ohne
|
||||
// Sitzungstabelle auszukommen. Wer eine Sitzung sofort beenden will, ändert
|
||||
// SESSION_SECRET; dann sind alle ungültig.
|
||||
func (ss *SessionStore) Touch(w http.ResponseWriter, r *http.Request) {
|
||||
sess, err := ss.store.Get(r, sessionName)
|
||||
if err != nil || sess.IsNew {
|
||||
return
|
||||
}
|
||||
if last, ok := sess.Values[sessionKeyTouched].(int64); ok {
|
||||
if time.Since(time.Unix(last, 0)) < refreshInterval {
|
||||
return
|
||||
}
|
||||
}
|
||||
sess.Values[sessionKeyTouched] = time.Now().Unix()
|
||||
sess.Options.Secure = middleware.IsHTTPS(r)
|
||||
// Ein Fehlschlag ist keiner, der den Aufruf scheitern lassen darf: Die
|
||||
// Sitzung gilt weiter, sie wurde nur nicht verlängert.
|
||||
_ = ss.store.Save(r, w, sess)
|
||||
}
|
||||
|
||||
func (ss *SessionStore) GetUserID(r *http.Request) (uuid.UUID, bool) {
|
||||
sess, err := ss.store.Get(r, sessionName)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user