Files
Aether/backend/internal/auth/session.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

119 lines
3.2 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 (
"crypto/rand"
"encoding/base64"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/gorilla/sessions"
"aether/pkg/middleware"
)
const (
sessionName = "aether_session"
sessionKeyUser = "user_id"
csrfCookieName = "aether_csrf"
csrfHeaderName = "X-CSRF-Token"
)
type SessionStore struct {
store *sessions.CookieStore
maxAge int
}
func NewSessionStore(secret string, maxAge int) *SessionStore {
store := sessions.NewCookieStore([]byte(secret))
store.Options = &sessions.Options{
Path: "/",
MaxAge: maxAge,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
return &SessionStore{store: store, maxAge: maxAge}
}
func (ss *SessionStore) SetUser(w http.ResponseWriter, r *http.Request, userID uuid.UUID) error {
sess, err := ss.store.Get(r, sessionName)
if err != nil {
sess, _ = ss.store.New(r, sessionName)
}
sess.Values[sessionKeyUser] = userID.String()
sess.Options.Secure = middleware.IsHTTPS(r)
return 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 {
return uuid.Nil, false
}
raw, ok := sess.Values[sessionKeyUser].(string)
if !ok {
return uuid.Nil, false
}
id, err := uuid.Parse(raw)
if err != nil {
return uuid.Nil, false
}
return id, true
}
func (ss *SessionStore) Clear(w http.ResponseWriter, r *http.Request) error {
sess, err := ss.store.Get(r, sessionName)
if err != nil {
return nil
}
sess.Options.MaxAge = -1
return ss.store.Save(r, w, sess)
}
// SetCSRFCookie sets a CSRF token as a readable (non-httpOnly) cookie.
func SetCSRFCookie(w http.ResponseWriter, r *http.Request) (string, error) {
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return "", err
}
encoded := base64.RawURLEncoding.EncodeToString(token)
http.SetCookie(w, &http.Cookie{
Name: csrfCookieName,
Value: encoded,
Path: "/",
HttpOnly: false, // must be readable by JS
Secure: middleware.IsHTTPS(r),
SameSite: http.SameSiteStrictMode,
})
return encoded, nil
}
// ValidateCSRF prüft, dass das mitgeschickte Token zum CSRF-Cookie passt
// (Double-Submit).
//
// Neben dem Header wird auch ein Formularfeld akzeptiert. Grund: Ein Download
// muss aus einer **Navigation** kommen, damit der Browser ihn als Datei
// behandelt statt ihn in den Speicher zu laden – und ein abgeschicktes
// Formular kann keine Header setzen. Am Schutz ändert das nichts: Verglichen
// wird weiterhin gegen das Cookie, an das eine fremde Seite nicht herankommt.
func ValidateCSRF(r *http.Request) bool {
cookie, err := r.Cookie(csrfCookieName)
if err != nil || cookie.Value == "" {
return false
}
if token := r.Header.Get(csrfHeaderName); token != "" {
return token == cookie.Value
}
// Nur bei Formularen nachsehen: Bei anderen Inhaltstypen würde ParseForm
// den Rumpf lesen, den der eigentliche Handler danach noch braucht.
ct := r.Header.Get("Content-Type")
if strings.HasPrefix(ct, "application/x-www-form-urlencoded") {
if err := r.ParseForm(); err != nil {
return false
}
token := r.PostFormValue("csrf_token")
return token != "" && token == cookie.Value
}
return false
}