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"