87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
// 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:<id>" — normale Team-Session
|
|
// - "admin" — Admin-Session ohne aktives Team (zeigt Team-Übersicht)
|
|
// - "admin:<id>" — 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
|
|
}
|