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>
119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
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
|
||
}
|