Files
Aether/backend/internal/auth/middleware.go
T
marcandClaude Opus 5 093ff5140e 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>
2026-09-01 01:14:13 +02:00

74 lines
2.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"context"
"net/http"
"aether/pkg/response"
)
type contextKey int
const keyUser contextKey = iota
// RequireAuth loads the session user and injects it into context.
// Returns 401 if not authenticated.
func RequireAuth(ss *SessionStore, store *Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id, ok := ss.GetUserID(r)
if !ok {
response.Error(w, http.StatusUnauthorized, "authentication required")
return
}
user, err := store.GetByID(r.Context(), id)
if err != nil {
response.Error(w, http.StatusUnauthorized, "authentication required")
return
}
// Die Frist läuft ab der letzten Benutzung, nicht ab dem
// Anmelden. **Vor** dem Handler, weil danach der Rumpf der
// Antwort schon geschrieben sein kann – ein Set-Cookie käme dann
// zu spät und fiele stillschweigend unter den Tisch.
ss.Touch(w, r)
ctx := context.WithValue(r.Context(), keyUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RequireAdmin additionally checks that the user is an admin.
func RequireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := UserFromContext(r.Context())
if user == nil || !user.IsAdmin {
response.Error(w, http.StatusForbidden, "admin access required")
return
}
next.ServeHTTP(w, r)
})
}
// CSRF validates the double-submit CSRF token for state-changing requests.
func CSRF(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
next.ServeHTTP(w, r)
return
}
if !ValidateCSRF(r) {
response.Error(w, http.StatusForbidden, "invalid CSRF token")
return
}
next.ServeHTTP(w, r)
})
}
// UserFromContext retrieves the authenticated user from context (may be nil).
func UserFromContext(ctx context.Context) *User {
u, _ := ctx.Value(keyUser).(*User)
return u
}