// Dieses File implementiert die signierte Web-Session für den Team-Login. // // Anders als beim bestehenden Seiten-Passwort-Feature (das den Klartext-Wert // direkt als Cookie speichert) wird hier NICHT der Zugriffskey im Cookie // abgelegt, sondern nur eine signierte Behauptung ("ich bin Team X, gültig bis Y"). // Der Server prüft bei jedem Request die Signatur (HMAC-SHA256) und das Ablaufdatum — // ein Angreifer kann den Cookie-Inhalt zwar lesen, aber nicht fälschen, weil er // das SESSION_SECRET nicht kennt. // // Liegt im handler-Package (nicht middleware), weil sowohl die Login-Handler hier // (Session erzeugen) als auch middleware.RequireTeamSession (Session prüfen) darauf // zugreifen müssen — middleware importiert handler, nicht umgekehrt. package handler import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "fmt" "strconv" "strings" "time" ) // SessionCookieName ist der Name des Cookies der die signierte Session trägt. const SessionCookieName = "wiki_session" // SessionTTL bestimmt wie lange eine Session ohne erneuten Login gültig bleibt. const SessionTTL = 30 * 24 * time.Hour // signPayload berechnet den HMAC-SHA256 eines Payloads mit dem Session-Secret. func signPayload(payload string, secret []byte) string { mac := hmac.New(sha256.New, secret) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } // SignSession erstellt einen signierten Cookie-Wert für das gegebene "subject". // // subject ist eine von drei Formen: // - "team:" — normale Team-Session // - "admin" — Admin-Session ohne aktives Team (zeigt Team-Übersicht) // - "admin:" — Admin-Session mit über /switch-team gewähltem aktivem Team func SignSession(subject string, secret []byte) string { expiry := time.Now().Add(SessionTTL).Unix() payload := fmt.Sprintf("%s|%d", subject, expiry) sig := signPayload(payload, secret) encoded := base64.RawURLEncoding.EncodeToString([]byte(payload)) return encoded + "." + sig } // VerifySession prüft Signatur und Ablaufdatum eines Cookie-Werts und gibt das // enthaltene subject zurück (siehe SignSession für das Format). func VerifySession(cookieValue string, secret []byte) (subject string, ok bool) { parts := strings.SplitN(cookieValue, ".", 2) if len(parts) != 2 { return "", false } payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return "", false } payload := string(payloadBytes) expectedSig := signPayload(payload, secret) if subtle.ConstantTimeCompare([]byte(parts[1]), []byte(expectedSig)) != 1 { return "", false } fields := strings.SplitN(payload, "|", 2) if len(fields) != 2 { return "", false } expiry, err := strconv.ParseInt(fields[1], 10, 64) if err != nil { return "", false } if time.Now().Unix() > expiry { return "", false // Session abgelaufen } return fields[0], true }