internal/totp/totp.go: RFC-6238-TOTP (HOTP RFC 4226) selbst implementiert mit stdlib crypto/hmac+sha1 — kein externes Modul. DefaultSkewSteps=1 dokumentiert die Zeitversatz-Toleranz (+/-30s um die Serverzeit, Akzeptanzkriterium/Pruefung 3). ProvisioningURI liefert die otpauth://-URI fuer den QR-Code (Akzeptanzkriterium 1) — das Rendering selbst ist Frontend-Sache (IAM-08). internal/totp/store.go: BeginSetup speichert ein neues Secret als NICHT bestaetigt; ConfirmSetup aktiviert 2FA erst nach einmaliger erfolgreicher Code-Eingabe (Akzeptanzkriterium 1) und erzeugt 10 Wiederherstellungscodes (nur Hash gespeichert, Klartext einmalig zurueckgegeben). VerifyLoginCode akzeptiert TOTP-Code ODER Wiederherstellungscode; consumeRecoveryCode markiert einen Code atomar als verwendet (WHERE used_at IS NULL) — kein doppeltes Einloesen moeglich (Akzeptanzkriterium 3). internal/totp/login.go: LoginWithTOTP komponiert IAM-02s LoginService, ohne ihn zu veraendern — ist 2FA fuer den Benutzer aktiv, wird ein fehlender/ falscher Code zuverlaessig abgewiesen, selbst bei korrektem Passwort (Akzeptanzkriterium 2). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Login-Versuch ohne zweiten Faktor bei aktivem 2FA zuverlaessig abgewiesen — TestLoginWithTOTP_RequiresSecondFactorWhenEnabled: korrektes Passwort ohne Code -> ErrSecondFactorRequired, mit gueltigem Code -> Token. PASS. 2. Wiederherstellungscode nach Nutzung als verbraucht getestet — TestVerifyLoginCode_RecoveryCodeIsSingleUse: erste Nutzung erfolgreich, zweite abgelehnt. PASS. 3. Zeitversatz-Toleranz dokumentiert und getestet — TestValidate_ClockSkewTolerance: Code aus 25s Vergangenheit gueltig (innerhalb dokumentierter Toleranz), Code aus 5min Vergangenheit ungueltig. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
1.9 KiB
Go
80 lines
1.9 KiB
Go
package totp
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGenerateAndValidateCode_RoundTrip(t *testing.T) {
|
|
secret, err := GenerateSecret()
|
|
if err != nil {
|
|
t.Fatalf("generate secret: %v", err)
|
|
}
|
|
now := time.Now()
|
|
code, err := GenerateCode(secret, now)
|
|
if err != nil {
|
|
t.Fatalf("generate code: %v", err)
|
|
}
|
|
if len(code) != 6 {
|
|
t.Fatalf("erwartet 6-stelligen code, habe %q", code)
|
|
}
|
|
|
|
ok, err := Validate(secret, code, now)
|
|
if err != nil {
|
|
t.Fatalf("validate: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("erwartet gueltigen code")
|
|
}
|
|
|
|
wrongCode := "000000"
|
|
if code == wrongCode {
|
|
wrongCode = "111111"
|
|
}
|
|
ok, err = Validate(secret, wrongCode, now)
|
|
if err != nil {
|
|
t.Fatalf("validate (falsch): %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatal("erwartet ungueltig fuer falschen code")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium/Pruefung 3: Zeitversatz-Toleranz dokumentiert (siehe
|
|
// DefaultSkewSteps = 1 Schritt = 30s) und getestet.
|
|
func TestValidate_ClockSkewTolerance(t *testing.T) {
|
|
secret, err := GenerateSecret()
|
|
if err != nil {
|
|
t.Fatalf("generate secret: %v", err)
|
|
}
|
|
now := time.Now()
|
|
|
|
// Code aus dem VORHERIGEN Zeitfenster (bis zu 30s in der Vergangenheit)
|
|
// muss innerhalb der dokumentierten Toleranz noch akzeptiert werden.
|
|
pastCode, err := GenerateCode(secret, now.Add(-25*time.Second))
|
|
if err != nil {
|
|
t.Fatalf("generate code (past): %v", err)
|
|
}
|
|
ok, err := Validate(secret, pastCode, now)
|
|
if err != nil {
|
|
t.Fatalf("validate (past): %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("erwartet gueltig innerhalb der dokumentierten skew-toleranz (25s)")
|
|
}
|
|
|
|
// Code weit ausserhalb der Toleranz (mehrere Schritte in der
|
|
// Vergangenheit) muss abgelehnt werden.
|
|
farPastCode, err := GenerateCode(secret, now.Add(-5*time.Minute))
|
|
if err != nil {
|
|
t.Fatalf("generate code (far past): %v", err)
|
|
}
|
|
ok, err = Validate(secret, farPastCode, now)
|
|
if err != nil {
|
|
t.Fatalf("validate (far past): %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatal("erwartet ungueltig weit ausserhalb der skew-toleranz (5min)")
|
|
}
|
|
}
|