109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SessionCookieName ist der Name des Cookies, der die signierte Sitzung trägt.
|
|
//
|
|
// Wird der Name geändert, gelten alle bestehenden Sitzungen als nicht vorhanden —
|
|
// jeder muss sich einmal neu anmelden. Harmlos, aber kein Grund, ihn ohne Anlass
|
|
// anzufassen.
|
|
const SessionCookieName = "munin_session"
|
|
|
|
// SessionTTL bestimmt, wie lange eine Sitzung ohne erneute Anmeldung gilt.
|
|
const SessionTTL = 30 * 24 * time.Hour
|
|
|
|
// SubjectPlatformAdmin ist das Session-Subject des Plattform-Admins (ADMIN_TOKEN).
|
|
const SubjectPlatformAdmin = "platform"
|
|
|
|
// SubjectKeyPrefix steht vor der Key-ID im Session-Subject einer normalen Anmeldung.
|
|
const SubjectKeyPrefix = "key:"
|
|
|
|
// Signer signiert und prüft alles, was der Server einem Browser mitgibt:
|
|
// Session-Cookies und CSRF-Tokens.
|
|
//
|
|
// Ein eigener Typ statt loser Funktionen, damit das Geheimnis genau einmal
|
|
// übergeben wird und nicht durch jeden Aufruf gereicht werden muss.
|
|
type Signer struct {
|
|
secret []byte
|
|
}
|
|
|
|
// NewSigner erzeugt einen Signer aus dem SESSION_SECRET.
|
|
func NewSigner(secret []byte) *Signer {
|
|
return &Signer{secret: secret}
|
|
}
|
|
|
|
// sign berechnet den HMAC-SHA256 einer Nutzlast.
|
|
//
|
|
// HMAC ist ein Prüfsummenverfahren mit Geheimnis: Wer das Geheimnis nicht kennt,
|
|
// kann zu einer Nutzlast keine passende Prüfsumme bilden. Damit kann der Server
|
|
// erkennen, ob ein Cookie-Inhalt von ihm selbst stammt oder unterwegs verändert wurde.
|
|
func (s *Signer) sign(payload string) string {
|
|
mac := hmac.New(sha256.New, s.secret)
|
|
mac.Write([]byte(payload))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// SignSession erstellt den Cookie-Wert für ein Subject.
|
|
//
|
|
// subject ist entweder SubjectPlatformAdmin oder "key:<api-key-id>".
|
|
//
|
|
// Aufbau: base64("<subject>|<ablauf-unix>") + "." + HMAC
|
|
//
|
|
// Der Cookie ist lesbar, aber nicht fälschbar. Er enthält bewusst keinen Key —
|
|
// wer ihn stiehlt, bekommt eine Sitzung, aber kein dauerhaftes Zugangsmittel für
|
|
// die API. Und ob der zugehörige Key überhaupt noch gültig ist, prüft der Server
|
|
// bei jedem Request neu in der Datenbank (siehe internal/web).
|
|
func (s *Signer) SignSession(subject string) string {
|
|
expiry := time.Now().Add(SessionTTL).Unix()
|
|
payload := fmt.Sprintf("%s|%d", subject, expiry)
|
|
encoded := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
|
return encoded + "." + s.sign(payload)
|
|
}
|
|
|
|
// VerifySession prüft Signatur und Ablaufdatum und gibt das Subject zurück.
|
|
func (s *Signer) VerifySession(cookieValue string) (subject string, ok bool) {
|
|
encoded, signature, found := strings.Cut(cookieValue, ".")
|
|
if !found {
|
|
return "", false
|
|
}
|
|
|
|
payloadBytes, err := base64.RawURLEncoding.DecodeString(encoded)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
payload := string(payloadBytes)
|
|
|
|
// Reihenfolge ist wichtig: erst die Signatur prüfen, dann den Inhalt auswerten.
|
|
// Alles vor dieser Zeile stammt aus einer nicht vertrauenswürdigen Quelle.
|
|
if subtle.ConstantTimeCompare([]byte(signature), []byte(s.sign(payload))) != 1 {
|
|
return "", false
|
|
}
|
|
|
|
subject, expiryStr, found := strings.Cut(payload, "|")
|
|
if !found {
|
|
return "", false
|
|
}
|
|
|
|
expiry, err := strconv.ParseInt(expiryStr, 10, 64)
|
|
if err != nil || time.Now().Unix() > expiry {
|
|
return "", false
|
|
}
|
|
|
|
return subject, true
|
|
}
|
|
|
|
// KeyIDFromSubject holt die API-Key-ID aus einem Session-Subject.
|
|
// Beim Plattform-Admin gibt es keine — dann ist ok false.
|
|
func KeyIDFromSubject(subject string) (keyID string, ok bool) {
|
|
return strings.CutPrefix(subject, SubjectKeyPrefix)
|
|
}
|