TASK-SECADV-001: WebAuthn/Passkeys (F022) - WebAuthn credential model, service, handler - Registration/authentication ceremony endpoints - CRUD operations (list, rename, delete passkeys) - Routes: GET/POST/PUT/DELETE /auth/passkeys/* TASK-SECADV-002: Configurable password policy (F015) - PasswordPolicyConfig with MinLength, MaxLength, RequireUpper/Lower/Number/Special - NewPasswordValidatorWithPolicy constructor - PasswordPolicyFromEnv() reads env vars (PASSWORD_MIN_LENGTH, etc.) - All character class checks now respect policy configuration TASK-SECADV-003: Géolocalisation connexions (F025) - GeoIPResolver interface + GeoIPService implementation - Country/city columns added to login_history table - LoginHistoryService.Record() performs GeoIP lookup - GetUserHistory returns geolocation data - GET /auth/login-history endpoint TASK-SECADV-004: Password expiration (F016) - password_changed_at column on users table - CheckPasswordExpiration() method on PasswordService - All password change/reset methods now set password_changed_at - NewPasswordServiceWithPolicy() supports expiration days config Migration: 971_security_advanced_v0133.sql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
69 lines
1.8 KiB
Go
69 lines
1.8 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestNewWebAuthnService(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
svc := NewWebAuthnService(nil, logger, "", "")
|
|
if svc.rpID != "localhost" {
|
|
t.Errorf("expected default rpID 'localhost', got %q", svc.rpID)
|
|
}
|
|
if svc.rpName != "Veza" {
|
|
t.Errorf("expected default rpName 'Veza', got %q", svc.rpName)
|
|
}
|
|
|
|
svc2 := NewWebAuthnService(nil, logger, "veza.fr", "Veza Platform")
|
|
if svc2.rpID != "veza.fr" {
|
|
t.Errorf("expected rpID 'veza.fr', got %q", svc2.rpID)
|
|
}
|
|
}
|
|
|
|
func TestWebAuthnChallengeGeneration(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
// Without DB, BeginRegistration will still generate challenge and options
|
|
svc := NewWebAuthnService(nil, logger, "veza.fr", "Veza")
|
|
|
|
userID := uuid.New()
|
|
challenge, options, err := svc.BeginRegistration(nil, userID, "testuser")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if challenge.Challenge == "" {
|
|
t.Error("expected non-empty challenge")
|
|
}
|
|
if challenge.Type != "registration" {
|
|
t.Errorf("expected type 'registration', got %q", challenge.Type)
|
|
}
|
|
if options == nil {
|
|
t.Error("expected non-nil options")
|
|
}
|
|
// Check RP info in options
|
|
rp, ok := options["rp"].(map[string]string)
|
|
if !ok {
|
|
t.Fatal("expected rp map in options")
|
|
}
|
|
if rp["id"] != "veza.fr" {
|
|
t.Errorf("expected rp.id 'veza.fr', got %q", rp["id"])
|
|
}
|
|
}
|
|
|
|
func TestWebAuthnFinishRegistrationValidation(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
svc := NewWebAuthnService(nil, logger, "localhost", "Veza")
|
|
|
|
// Empty credential should fail
|
|
_, err := svc.FinishRegistration(nil, uuid.New(), nil, nil, nil, "none", "")
|
|
if err == nil {
|
|
t.Error("expected error for empty credential data")
|
|
}
|
|
|
|
_, err = svc.FinishRegistration(nil, uuid.New(), []byte("cred"), nil, nil, "none", "")
|
|
if err == nil {
|
|
t.Error("expected error for empty public key")
|
|
}
|
|
}
|