internal/auth: Login/Logout ueber httpOnly/Secure/SameSite=Strict-Cookie mit HS256-JWT (30min TTL), bcrypt-Passwort-Hashing (Cost 12, explizit begruendet und benchmarkt statt DefaultCost uebernommen), RequireAuth-Middleware fuer geschuetzte Routen. LoginService ist strukturell auf einen Tenant gescopt (nutzt user.TenantUserStore, dessen Pool = eine Tenant-DB — derselbe Mechanismus wie in TEN-01/TEN-02), liefert bei falscher E-Mail und falschem Passwort denselben Fehler (User-Enumeration-Schutz) inkl. Dummy-bcrypt- Vergleich gegen Timing-Seitenkanal bei unbekannter E-Mail. user.TenantUserStore erweitert um SetPasswordHash/GetByEmailForAuth (password_hash bleibt ausserhalb des regulaeren User-Typs/JSON-Pfads). Migration 0002 fuegt password_hash-Spalte hinzu (Default '', da IAM-01 User ohne Passwort anlegt). Login-Handler ist wie IAM-01/TEN-02 aus denselben Gruenden (Tenant- Connection-Routing = TEN-06, noch nicht gebaut) nicht in cmd/core/main.go verdrahtet — Package ist eigenstaendig nutzbar/getestet. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Login-Query tenant-gescopt — TestLoginService_NoCrossTenantLogin: gleiche E-Mail in zwei Tenant-DBs mit unterschiedlichem Passwort, Login gegen Tenant A mit Tenant-B-Passwort schlaegt fehl. PASS. 2. Session-Fixation/Token-Manipulation — TestTokenVerify_RejectsManipulatedPayload und TestTokenVerify_RejectsWrongSecret: manipuliertes/falsch signiertes Token wird abgelehnt. PASS. 3. Abgelaufenes Token erzwingt Neuanmeldung — TestTokenVerify_RejectsExpiredToken und TestRequireAuth_BlocksWithoutValidCookie. PASS. 4. Login-Latenz mit Kostenfaktor 12 gemessen: 294ms (Ziel < 400ms) — TestBcryptCostAgainstLatencyTarget. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
2.1 KiB
Go
83 lines
2.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
func TestTokenIssueAndVerify(t *testing.T) {
|
|
issuer := NewTokenIssuer("test-secret-nur-fuer-tests")
|
|
|
|
token, err := issuer.Issue("user-1", "acme")
|
|
if err != nil {
|
|
t.Fatalf("issue: %v", err)
|
|
}
|
|
|
|
claims, err := issuer.Verify(token)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if claims.UserID != "user-1" || claims.TenantSlug != "acme" {
|
|
t.Fatalf("claims unerwartet: %+v", claims)
|
|
}
|
|
}
|
|
|
|
// Pruefung 2: Token-Manipulationstest.
|
|
func TestTokenVerify_RejectsManipulatedPayload(t *testing.T) {
|
|
issuer := NewTokenIssuer("test-secret-nur-fuer-tests")
|
|
token, err := issuer.Issue("user-1", "acme")
|
|
if err != nil {
|
|
t.Fatalf("issue: %v", err)
|
|
}
|
|
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 3 {
|
|
t.Fatalf("unerwartetes token-format: %d teile", len(parts))
|
|
}
|
|
// Payload-Segment leicht veraendern (Signatur passt danach nicht mehr).
|
|
tampered := parts[0] + "." + parts[1] + "x" + "." + parts[2]
|
|
|
|
if _, err := issuer.Verify(tampered); err == nil {
|
|
t.Fatal("erwartet fehler bei manipuliertem token, habe nil")
|
|
}
|
|
}
|
|
|
|
func TestTokenVerify_RejectsWrongSecret(t *testing.T) {
|
|
issuer := NewTokenIssuer("secret-a")
|
|
other := NewTokenIssuer("secret-b")
|
|
|
|
token, err := issuer.Issue("user-1", "acme")
|
|
if err != nil {
|
|
t.Fatalf("issue: %v", err)
|
|
}
|
|
if _, err := other.Verify(token); err == nil {
|
|
t.Fatal("erwartet fehler bei falschem secret, habe nil")
|
|
}
|
|
}
|
|
|
|
// Pruefung 3: abgelaufenes Token erzwingt Neuanmeldung.
|
|
func TestTokenVerify_RejectsExpiredToken(t *testing.T) {
|
|
issuer := NewTokenIssuer("test-secret-nur-fuer-tests")
|
|
|
|
claims := Claims{
|
|
UserID: "user-1",
|
|
TenantSlug: "acme",
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
IssuedAt: jwt.NewNumericDate(time.Now().Add(-2 * AccessTokenTTL)),
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Minute)),
|
|
},
|
|
}
|
|
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
expired, err := tok.SignedString([]byte("test-secret-nur-fuer-tests"))
|
|
if err != nil {
|
|
t.Fatalf("signieren: %v", err)
|
|
}
|
|
|
|
if _, err := issuer.Verify(expired); err == nil {
|
|
t.Fatal("erwartet fehler bei abgelaufenem token, habe nil")
|
|
}
|
|
}
|