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 }