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>
57 lines
2.1 KiB
Go
57 lines
2.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/user"
|
|
)
|
|
|
|
var ErrInvalidCredentials = errors.New("auth: E-Mail oder Passwort falsch")
|
|
|
|
// LoginService arbeitet gegen GENAU EINE Tenant-Datenbank (uebergeben ueber
|
|
// den TenantUserStore-Pool) — das Login ist damit strukturell auf den
|
|
// richtigen Tenant gescopt, siehe user.TenantUserStore.GetByEmailForAuth.
|
|
type LoginService struct {
|
|
users *user.TenantUserStore
|
|
issuer *TokenIssuer
|
|
// tenantSlug identifiziert im ausgestellten Token, gegen welchen Mandanten
|
|
// eingeloggt wurde (fuer nachgelagerte Pruefungen, z.B. Middleware-Logs).
|
|
tenantSlug string
|
|
}
|
|
|
|
func NewLoginService(users *user.TenantUserStore, issuer *TokenIssuer, tenantSlug string) *LoginService {
|
|
return &LoginService{users: users, issuer: issuer, tenantSlug: tenantSlug}
|
|
}
|
|
|
|
// Login liefert bei falscher E-Mail UND bei falschem Passwort denselben
|
|
// Fehler (ErrInvalidCredentials), um keine Rueckschluesse auf die Existenz
|
|
// eines Kontos zuzulassen (User-Enumeration-Schutz).
|
|
func (s *LoginService) Login(ctx context.Context, email, password string) (string, error) {
|
|
creds, err := s.users.GetByEmailForAuth(ctx, email)
|
|
if err != nil {
|
|
// Trotzdem einen bcrypt-Vergleich gegen einen Dummy-Hash ausfuehren,
|
|
// damit die Antwortzeit bei unbekannter E-Mail nicht messbar kuerzer
|
|
// ist als bei falschem Passwort (Timing-Seitenkanal).
|
|
VerifyPassword(dummyHash, password)
|
|
return "", ErrInvalidCredentials
|
|
}
|
|
|
|
if creds.User.Status != user.StatusActive {
|
|
return "", ErrInvalidCredentials
|
|
}
|
|
|
|
if !VerifyPassword(creds.PasswordHash, password) {
|
|
return "", ErrInvalidCredentials
|
|
}
|
|
|
|
return s.issuer.Issue(creds.User.ID, s.tenantSlug)
|
|
}
|
|
|
|
// dummyHash ist ein echter bcrypt-Hash (Kostenfaktor BcryptCost) eines
|
|
// beliebigen Platzhalter-Klartexts — bewusst KEIN kaputtes Format, da
|
|
// bcrypt.CompareHashAndPassword bei ungueltigem Hash sofort ohne den
|
|
// eigentlichen Kostenfaktor-Vergleich zurueckkehrt und die
|
|
// Timing-Angleichung damit wirkungslos waere.
|
|
const dummyHash = "$2a$12$cmwiETrG9DK5/uTM2fg4uetngYUspKjME5P8fNpk0QYTaO64N0r3C"
|