CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
Backend (kleinstmoeglich, wie im Ticket vorgesehen): - internal/auth: TokenIssuer.IssueWithTTL, LoginService.LoginWithTTL (additiv, bestehende Issue/Login unveraendert) - internal/totp: LoginWithTOTPTTL (durchlaeuft denselben 2FA-Ablauf) - cmd/account-api: neue Route POST /auth/mobile-login (JSON-Token statt Cookie, 90 Tage TTL statt 30 Minuten) Android-App (android/dms-capture, Kotlin, Gradle-Projekt): - CameraX-Erfassung mit Live-Vorschau, erneuter Aufnahme, Mehrseiten- Sammlung (AC2) - Ruft ausschliesslich die bestehende DOC-01-Upload-API auf, kein neuer Backend-Uploadpfad (AC3) -- echter Architektur-Fund dabei: dms-app erwartet ein audience-gebundenes Modul-Token (aud=dms), kein rohes Sitzungstoken; App tauscht ueber den bereits bestehenden Endpunkt POST /auth/module-token (Cookie-Header), dokumentiert im Pruefprotokoll - Room-basierte Offline-Warteschlange mit sichtbarem Status, WorkManager fuer automatische Nachreichung ohne Nutzeraktion (AC4) - Verstaendliche Fehlermeldungen bei Groessen-/Typ-Ablehnung statt nur Fehlercode (AC5) - Internes APK, kein Play-Store-Zwang (AC6) Real getestet: Ende-zu-Ende-Auth-Kette (Login -> Modul-Token -> Bearer gegen echten dms-app) auf Host 204, 13/13 Kotlin-Unit-Tests gruen, echter APK-Build auf Host 236 (Android-SDK dort vorhanden, kein Install auf der Entwicklungsmaschine, Gradle projektlokal nachgezogen statt System-Install). Offen (Pruefprotokoll): Kamera-/Offline-/Smoke-Install-Pruefungen brauchen einen Android-Emulator; kein System-Image auf Host 236 installiert, Nachinstallation bewusst nicht ohne Rueckfrage vorgenommen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
117 lines
3.2 KiB
Go
117 lines
3.2 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")
|
|
}
|
|
}
|
|
|
|
// INT-21 AC1: langlebiges Token fuer native Clients hat eine andere
|
|
// Gueltigkeit als das normale Sitzungstoken, bleibt sonst identisch pruefbar.
|
|
func TestTokenIssueWithTTL(t *testing.T) {
|
|
issuer := NewTokenIssuer("test-secret-nur-fuer-tests")
|
|
|
|
longToken, err := issuer.IssueWithTTL("user-1", "acme", 90*24*time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("issue: %v", err)
|
|
}
|
|
claims, err := issuer.Verify(longToken)
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
if claims.UserID != "user-1" || claims.TenantSlug != "acme" {
|
|
t.Fatalf("claims unerwartet: %+v", claims)
|
|
}
|
|
remaining := time.Until(claims.ExpiresAt.Time)
|
|
if remaining < 89*24*time.Hour {
|
|
t.Fatalf("erwartete ca. 90 tage restlaufzeit, war %v", remaining)
|
|
}
|
|
}
|
|
|
|
// INT-21 Pruefung 1: abgelaufenes langlebiges Token wird abgewiesen.
|
|
func TestTokenIssueWithTTL_RejectsExpired(t *testing.T) {
|
|
issuer := NewTokenIssuer("test-secret-nur-fuer-tests")
|
|
token, err := issuer.IssueWithTTL("user-1", "acme", -time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("issue: %v", err)
|
|
}
|
|
if _, err := issuer.Verify(token); err == nil {
|
|
t.Fatal("erwartete fehler fuer abgelaufenes token")
|
|
}
|
|
}
|