1008 lines
39 KiB
Go
1008 lines
39 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
webauthnlib "github.com/go-webauthn/webauthn/webauthn"
|
|
|
|
"wucher/internal/config"
|
|
"wucher/internal/domain/auth"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
)
|
|
|
|
const (
|
|
testCredentialCreationResponseJSON = `{
|
|
"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
|
|
"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
|
|
"type":"public-key",
|
|
"authenticatorAttachment":"platform",
|
|
"clientExtensionResults":{"appid":true},
|
|
"response":{
|
|
"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
|
|
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
|
|
"transports":["usb","nfc","fake"]
|
|
}
|
|
}`
|
|
testCredentialAssertionResponseJSON = `{
|
|
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"clientExtensionResults":{"appID":"example.com"},
|
|
"type":"public-key",
|
|
"response":{
|
|
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
|
|
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
|
|
"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
|
|
"userHandle":"0ToAAAAAAAAAAA"
|
|
}
|
|
}`
|
|
)
|
|
|
|
type webAuthnRepoWithErrors struct {
|
|
*mockAuthRepo
|
|
listCredsErr error
|
|
createCredErr error
|
|
updateCredErr error
|
|
}
|
|
|
|
func newWebAuthnRepoWithErrors() *webAuthnRepoWithErrors {
|
|
return &webAuthnRepoWithErrors{mockAuthRepo: newMockAuthRepo()}
|
|
}
|
|
|
|
func (r *webAuthnRepoWithErrors) ListUserWebAuthnCredentials(ctx context.Context, userID []byte) ([]auth.UserWebAuthnCredential, error) {
|
|
if r.listCredsErr != nil {
|
|
return nil, r.listCredsErr
|
|
}
|
|
return r.mockAuthRepo.ListUserWebAuthnCredentials(ctx, userID)
|
|
}
|
|
|
|
func (r *webAuthnRepoWithErrors) CreateUserWebAuthnCredential(ctx context.Context, credential *auth.UserWebAuthnCredential) error {
|
|
if r.createCredErr != nil {
|
|
return r.createCredErr
|
|
}
|
|
return r.mockAuthRepo.CreateUserWebAuthnCredential(ctx, credential)
|
|
}
|
|
|
|
func (r *webAuthnRepoWithErrors) UpdateUserWebAuthnCredential(ctx context.Context, userID, credentialID, credentialJSON []byte, usedAt time.Time) error {
|
|
if r.updateCredErr != nil {
|
|
return r.updateCredErr
|
|
}
|
|
return r.mockAuthRepo.UpdateUserWebAuthnCredential(ctx, userID, credentialID, credentialJSON, usedAt)
|
|
}
|
|
|
|
func newConfiguredWebAuthnService(repo auth.Repository, tokens TokenStore) *AuthService {
|
|
return NewAuthService(
|
|
repo,
|
|
newMockRoleRepo(),
|
|
tokens,
|
|
nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnRPID: "example.com",
|
|
WebAuthnRPDisplayName: "Wucher Test",
|
|
WebAuthnRPOrigins: []string{"https://example.com"},
|
|
WebAuthnChallengeTTL: 2 * time.Minute,
|
|
}},
|
|
)
|
|
}
|
|
|
|
func putWebAuthnChallengeToken(t *testing.T, tokens *mockTokenStore, token, ceremony string, userID []byte, expires time.Time) {
|
|
t.Helper()
|
|
envelope := webAuthnChallengeEnvelope{
|
|
Ceremony: ceremony,
|
|
UserID: append([]byte(nil), userID...),
|
|
Session: webauthnlib.SessionData{
|
|
Challenge: "challenge",
|
|
RelyingPartyID: "example.com",
|
|
UserID: append([]byte(nil), userID...),
|
|
Expires: expires,
|
|
},
|
|
}
|
|
payload, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
t.Fatalf("marshal envelope: %v", err)
|
|
}
|
|
tokens.store[webAuthnChallengeKey(token)] = payload
|
|
}
|
|
|
|
func TestWebAuthnUserMethods(t *testing.T) {
|
|
user := webAuthnUser{
|
|
id: []byte{1, 2, 3},
|
|
name: "user@example.com",
|
|
displayName: "User Example",
|
|
credentials: []webauthnlib.Credential{{ID: []byte{9, 8, 7}}},
|
|
}
|
|
|
|
id := user.WebAuthnID()
|
|
if !bytes.Equal(id, []byte{1, 2, 3}) {
|
|
t.Fatalf("unexpected id: %v", id)
|
|
}
|
|
id[0] = 99
|
|
if user.id[0] == 99 {
|
|
t.Fatalf("expected returned id to be copied")
|
|
}
|
|
|
|
if user.WebAuthnName() != "user@example.com" {
|
|
t.Fatalf("unexpected name")
|
|
}
|
|
if user.WebAuthnDisplayName() != "User Example" {
|
|
t.Fatalf("unexpected display name")
|
|
}
|
|
|
|
creds := user.WebAuthnCredentials()
|
|
if len(creds) != 1 {
|
|
t.Fatalf("unexpected credential length")
|
|
}
|
|
creds = append(creds, webauthnlib.Credential{})
|
|
if len(user.credentials) != 1 {
|
|
t.Fatalf("expected credential slice header to be copied")
|
|
}
|
|
}
|
|
|
|
func TestBuildWebAuthnUser(t *testing.T) {
|
|
uid := uuidv7.MustBytes()
|
|
credential := webauthnlib.Credential{ID: []byte{1, 2, 3}}
|
|
user := &auth.User{
|
|
ID: uid,
|
|
Email: " alice@example.com ",
|
|
FirstName: " Alice ",
|
|
LastName: " Smith ",
|
|
IsActive: true,
|
|
}
|
|
waUser := buildWebAuthnUser(user, []webauthnlib.Credential{credential})
|
|
if waUser.WebAuthnName() != "alice@example.com" {
|
|
t.Fatalf("unexpected webauthn name")
|
|
}
|
|
if waUser.WebAuthnDisplayName() != "Alice Smith" {
|
|
t.Fatalf("unexpected webauthn display name")
|
|
}
|
|
if !bytes.Equal(waUser.WebAuthnID(), uid) {
|
|
t.Fatalf("unexpected webauthn id")
|
|
}
|
|
|
|
fallback := buildWebAuthnUser(&auth.User{
|
|
ID: uid,
|
|
Email: "bob@example.com",
|
|
FirstName: " ",
|
|
LastName: " ",
|
|
IsActive: true,
|
|
}, nil)
|
|
if fallback.WebAuthnDisplayName() != "bob@example.com" {
|
|
t.Fatalf("expected display name fallback to email")
|
|
}
|
|
}
|
|
|
|
func TestAuthServiceIsWebAuthnConfigured(t *testing.T) {
|
|
t.Run("no credentials", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
ok, err := svc.IsWebAuthnConfigured(context.Background(), uuidv7.MustBytes())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatalf("expected not configured")
|
|
}
|
|
})
|
|
|
|
t.Run("has credentials", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
ID: uuidv7.MustBytes(),
|
|
UserID: userID,
|
|
CredentialID: []byte("cred-1"),
|
|
}}
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
ok, err := svc.IsWebAuthnConfigured(context.Background(), userID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("expected configured")
|
|
}
|
|
})
|
|
|
|
t.Run("repo error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.listCredsErr = errors.New("list failed")
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
ok, err := svc.IsWebAuthnConfigured(context.Background(), uuidv7.MustBytes())
|
|
if !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatalf("expected configured false on error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceWebAuthnChallengeTTL(t *testing.T) {
|
|
svcDefault := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnChallengeTTL: 0,
|
|
}})
|
|
if got := svcDefault.WebAuthnChallengeTTL(); got != 5*time.Minute {
|
|
t.Fatalf("expected default ttl 5m, got %v", got)
|
|
}
|
|
|
|
svcCustom := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnChallengeTTL: 42 * time.Second,
|
|
}})
|
|
if got := svcCustom.WebAuthnChallengeTTL(); got != 42*time.Second {
|
|
t.Fatalf("expected custom ttl, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestAuthServiceWebAuthnClient(t *testing.T) {
|
|
t.Run("missing rpid", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnRPOrigins: []string{"https://example.com"},
|
|
}})
|
|
if _, err := svc.webAuthnClient(); !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing origins", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnRPID: "example.com",
|
|
}})
|
|
if _, err := svc.webAuthnClient(); !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success trims and defaults", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), nil, nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnRPID: " example.com ",
|
|
WebAuthnRPDisplayName: " ",
|
|
WebAuthnRPOrigins: []string{" ", " https://example.com "},
|
|
WebAuthnChallengeTTL: 30 * time.Second,
|
|
}})
|
|
client, err := svc.webAuthnClient()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if client == nil || client.Config == nil {
|
|
t.Fatalf("expected webauthn client")
|
|
}
|
|
if client.Config.RPID != "example.com" {
|
|
t.Fatalf("unexpected rpid: %q", client.Config.RPID)
|
|
}
|
|
if client.Config.RPDisplayName != "Wucher" {
|
|
t.Fatalf("expected default rp display name, got %q", client.Config.RPDisplayName)
|
|
}
|
|
if len(client.Config.RPOrigins) != 1 || client.Config.RPOrigins[0] != "https://example.com" {
|
|
t.Fatalf("unexpected rp origins: %#v", client.Config.RPOrigins)
|
|
}
|
|
if client.Config.Timeouts.Login.Timeout != 30*time.Second {
|
|
t.Fatalf("unexpected login timeout: %v", client.Config.Timeouts.Login.Timeout)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceStoreWebAuthnChallenge(t *testing.T) {
|
|
t.Run("nil token store", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), nil)
|
|
session := &webauthnlib.SessionData{Challenge: "a"}
|
|
if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, []byte{1}, session); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil session", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, []byte{1}, nil); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("random token failure", func(t *testing.T) {
|
|
orig := rand.Reader
|
|
rand.Reader = errReader{}
|
|
defer func() { rand.Reader = orig }()
|
|
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
session := &webauthnlib.SessionData{Challenge: "abc"}
|
|
if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyRegistration, []byte{1}, session); !errors.Is(err, io.ErrUnexpectedEOF) {
|
|
t.Fatalf("expected random token error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("token store set error", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
tokens.setErr = errors.New("set failed")
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
session := &webauthnlib.SessionData{Challenge: "abc"}
|
|
if _, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyRegistration, []byte{1, 2}, session); !errors.Is(err, tokens.setErr) {
|
|
t.Fatalf("expected set error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
userID := uuidv7.MustBytes()
|
|
session := &webauthnlib.SessionData{
|
|
Challenge: "abc",
|
|
RelyingPartyID: "example.com",
|
|
UserID: userID,
|
|
Expires: time.Now().UTC().Add(time.Minute),
|
|
}
|
|
token, err := svc.storeWebAuthnChallenge(context.Background(), webAuthnCeremonyLogin, userID, session)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if strings.TrimSpace(token) == "" {
|
|
t.Fatalf("expected token")
|
|
}
|
|
if !strings.HasPrefix(tokens.lastKey, "auth:webauthn:challenge:") {
|
|
t.Fatalf("unexpected key: %q", tokens.lastKey)
|
|
}
|
|
|
|
raw := tokens.store[tokens.lastKey]
|
|
var envelope webAuthnChallengeEnvelope
|
|
if err := json.Unmarshal(raw, &envelope); err != nil {
|
|
t.Fatalf("unmarshal envelope: %v", err)
|
|
}
|
|
if envelope.Ceremony != webAuthnCeremonyLogin || !bytes.Equal(envelope.UserID, userID) {
|
|
t.Fatalf("unexpected envelope")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceConsumeWebAuthnChallenge(t *testing.T) {
|
|
t.Run("empty token", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), " ", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil token store", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), nil)
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "token", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("get error", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
tokens.getErr = errors.New("token store down")
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "token", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing token", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "missing", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid json", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
tokens.store[webAuthnChallengeKey("badjson")] = []byte("{bad")
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "badjson", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ceremony mismatch", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
putWebAuthnChallengeToken(t, tokens, "mismatch", webAuthnCeremonyRegistration, uuidv7.MustBytes(), time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "mismatch", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("expired", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
putWebAuthnChallengeToken(t, tokens, "expired", webAuthnCeremonyLogin, uuidv7.MustBytes(), time.Now().UTC().Add(-time.Second))
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
if _, err := svc.consumeWebAuthnChallenge(context.Background(), "expired", webAuthnCeremonyLogin); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success deletes token", func(t *testing.T) {
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
putWebAuthnChallengeToken(t, tokens, "ok", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), tokens)
|
|
envelope, err := svc.consumeWebAuthnChallenge(context.Background(), "ok", webAuthnCeremonyLogin)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if envelope == nil || !bytes.Equal(envelope.UserID, userID) {
|
|
t.Fatalf("unexpected envelope")
|
|
}
|
|
if _, found := tokens.store[webAuthnChallengeKey("ok")]; found {
|
|
t.Fatalf("expected challenge token to be deleted")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWebAuthnChallengeKey(t *testing.T) {
|
|
if got := webAuthnChallengeKey("abc"); got != "auth:webauthn:challenge:abc" {
|
|
t.Fatalf("unexpected key: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAuthServiceLoadWebAuthnCredentials(t *testing.T) {
|
|
t.Run("repo list error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.listCredsErr = errors.New("list creds failed")
|
|
svc := newConfiguredWebAuthnService(repo, nil)
|
|
if _, err := svc.loadWebAuthnCredentials(context.Background(), uuidv7.MustBytes()); !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected list error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid credential json", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1},
|
|
CredentialJSON: []byte("{bad"),
|
|
}}
|
|
svc := newConfiguredWebAuthnService(repo, nil)
|
|
if _, err := svc.loadWebAuthnCredentials(context.Background(), userID); err == nil {
|
|
t.Fatalf("expected unmarshal error")
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
serialized, err := json.Marshal(webauthnlib.Credential{
|
|
ID: []byte{1, 2, 3},
|
|
PublicKey: []byte{4, 5, 6},
|
|
AttestationType: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1, 2, 3},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
svc := newConfiguredWebAuthnService(repo, nil)
|
|
creds, err := svc.loadWebAuthnCredentials(context.Background(), userID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(creds) != 1 || !bytes.Equal(creds[0].ID, []byte{1, 2, 3}) {
|
|
t.Fatalf("unexpected credentials")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceBeginWebAuthnRegistrationBranches(t *testing.T) {
|
|
t.Run("invalid user id length", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), []byte("short")); !errors.Is(err, ErrInvalidToken) {
|
|
t.Fatalf("expected invalid token, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("not configured", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user fetch error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.getByIDErr = errors.New("db failed")
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, repo.getByIDErr) {
|
|
t.Fatalf("expected repo error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user not found", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), uuidv7.MustBytes()); !errors.Is(err, ErrInvalidToken) {
|
|
t.Fatalf("expected invalid token, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("load credentials error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
repo.listCredsErr = errors.New("list creds failed")
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), userID); !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected list creds error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("token store unavailable", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
svc := newConfiguredWebAuthnService(repo, nil)
|
|
if _, _, err := svc.BeginWebAuthnRegistration(context.Background(), userID); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success with exclusions", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{
|
|
ID: userID,
|
|
Email: "user@example.com",
|
|
FirstName: "Web",
|
|
LastName: "Authn",
|
|
IsActive: true,
|
|
}
|
|
|
|
serialized, err := json.Marshal(webauthnlib.Credential{
|
|
ID: []byte{8, 8, 8},
|
|
PublicKey: []byte{1, 2, 3},
|
|
AttestationType: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{8, 8, 8},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
creation, challengeToken, err := svc.BeginWebAuthnRegistration(context.Background(), userID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if creation == nil || strings.TrimSpace(challengeToken) == "" {
|
|
t.Fatalf("expected creation options and challenge token")
|
|
}
|
|
if len(creation.Response.CredentialExcludeList) != 1 {
|
|
t.Fatalf("expected one exclusion descriptor")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceFinishWebAuthnRegistrationBranches(t *testing.T) {
|
|
t.Run("invalid user id length", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newMockAuthRepo(), newMockTokenStore())
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), []byte("short"), "token", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrInvalidToken) {
|
|
t.Fatalf("expected invalid token, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("not configured", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), uuidv7.MustBytes(), "token", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid challenge", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "missing", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("challenge user mismatch", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
putWebAuthnChallengeToken(t, tokens, "tok1", webAuthnCeremonyRegistration, uuidv7.MustBytes(), time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok1", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected challenge invalid, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user fetch error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.getByIDErr = errors.New("db failed")
|
|
putWebAuthnChallengeToken(t, tokens, "tokfetch", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tokfetch", []byte("{}"), "name")
|
|
if !errors.Is(err, repo.getByIDErr) {
|
|
t.Fatalf("expected user fetch error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user not found", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
putWebAuthnChallengeToken(t, tokens, "tok2", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok2", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrInvalidToken) {
|
|
t.Fatalf("expected invalid token, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("load credentials error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
repo.listCredsErr = errors.New("list creds failed")
|
|
putWebAuthnChallengeToken(t, tokens, "tok3", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok3", []byte("{}"), "name")
|
|
if !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected list creds error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid payload", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
putWebAuthnChallengeToken(t, tokens, "tok4", webAuthnCeremonyRegistration, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
err := svc.FinishWebAuthnRegistration(context.Background(), userID, "tok4", []byte("{}"), "name")
|
|
if !errors.Is(err, ErrWebAuthnPayloadInvalid) {
|
|
t.Fatalf("expected payload invalid, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("verification failed", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{
|
|
ID: userID,
|
|
Email: "user@example.com",
|
|
FirstName: "A",
|
|
LastName: "B",
|
|
IsActive: true,
|
|
}
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
_, challengeToken, err := svc.BeginWebAuthnRegistration(context.Background(), userID)
|
|
if err != nil {
|
|
t.Fatalf("begin registration: %v", err)
|
|
}
|
|
err = svc.FinishWebAuthnRegistration(context.Background(), userID, challengeToken, []byte(testCredentialCreationResponseJSON), "Device A")
|
|
if !errors.Is(err, ErrWebAuthnVerificationFailed) {
|
|
t.Fatalf("expected verification failed, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success persists credential", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{
|
|
ID: userID,
|
|
Email: "user@example.com",
|
|
FirstName: "Web",
|
|
LastName: "Authn",
|
|
IsActive: true,
|
|
}
|
|
|
|
svc := NewAuthService(
|
|
repo,
|
|
newMockRoleRepo(),
|
|
tokens,
|
|
nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
|
|
WebAuthnRPID: "webauthn.io",
|
|
WebAuthnRPDisplayName: "Wucher Test",
|
|
WebAuthnRPOrigins: []string{"https://webauthn.io"},
|
|
WebAuthnChallengeTTL: 2 * time.Minute,
|
|
}},
|
|
)
|
|
|
|
envelope := webAuthnChallengeEnvelope{
|
|
Ceremony: webAuthnCeremonyRegistration,
|
|
UserID: append([]byte(nil), userID...),
|
|
Session: webauthnlib.SessionData{
|
|
Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
|
|
RelyingPartyID: "webauthn.io",
|
|
UserID: append([]byte(nil), userID...),
|
|
Expires: time.Now().UTC().Add(time.Minute),
|
|
CredParams: webauthnlib.CredentialParametersDefault(),
|
|
},
|
|
}
|
|
payload, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
t.Fatalf("marshal envelope: %v", err)
|
|
}
|
|
tokens.store[webAuthnChallengeKey("toksuccess")] = payload
|
|
|
|
if err := svc.FinishWebAuthnRegistration(context.Background(), userID, "toksuccess", []byte(testCredentialCreationResponseJSON), " Device A "); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
records := repo.webauthnCredentials[string(userID)]
|
|
if len(records) != 1 {
|
|
t.Fatalf("expected one stored webauthn credential, got %d", len(records))
|
|
}
|
|
if records[0].Name != "Device A" {
|
|
t.Fatalf("expected trimmed credential name, got %q", records[0].Name)
|
|
}
|
|
if len(records[0].CredentialID) == 0 || len(records[0].CredentialJSON) == 0 {
|
|
t.Fatalf("expected persisted credential data")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceBeginWebAuthnLoginBranches(t *testing.T) {
|
|
t.Run("not configured", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user fetch error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.getByMailErr = errors.New("db failed")
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, repo.getByMailErr) {
|
|
t.Fatalf("expected db error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user not found", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newWebAuthnRepoWithErrors(), newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrInvalidCredentials) {
|
|
t.Fatalf("expected invalid credentials, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("email not verified", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), " user@example.com "); !errors.Is(err, ErrEmailNotVerified) {
|
|
t.Fatalf("expected email not verified, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("load credentials error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
now := time.Now().UTC()
|
|
repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
|
|
repo.listCredsErr = errors.New("list creds failed")
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected list creds error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("no credentials configured", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
now := time.Now().UTC()
|
|
repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
|
|
svc := newConfiguredWebAuthnService(repo, newMockTokenStore())
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnCredentialUnavailable) {
|
|
t.Fatalf("expected credential unavailable, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("token store unavailable", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
userID := uuidv7.MustBytes()
|
|
now := time.Now().UTC()
|
|
repo.usersByMail["user@example.com"] = &auth.User{ID: userID, Email: "user@example.com", EmailVerifiedAt: &now, IsActive: true}
|
|
serialized, err := json.Marshal(webauthnlib.Credential{ID: []byte{1, 2, 3}, PublicKey: []byte{4}})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1, 2, 3},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
svc := newConfiguredWebAuthnService(repo, nil)
|
|
if _, _, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com"); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
now := time.Now().UTC()
|
|
repo.usersByMail["user@example.com"] = &auth.User{
|
|
ID: userID,
|
|
Email: "user@example.com",
|
|
EmailVerifiedAt: &now,
|
|
FirstName: "Web",
|
|
LastName: "User",
|
|
IsActive: true,
|
|
}
|
|
serialized, err := json.Marshal(webauthnlib.Credential{
|
|
ID: []byte{1, 2, 3},
|
|
PublicKey: []byte{4, 5, 6},
|
|
AttestationType: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1, 2, 3},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
assertion, token, err := svc.BeginWebAuthnLogin(context.Background(), " user@example.com ")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if assertion == nil || strings.TrimSpace(token) == "" {
|
|
t.Fatalf("expected assertion and challenge token")
|
|
}
|
|
if len(assertion.Response.AllowedCredentials) != 1 {
|
|
t.Fatalf("expected allowed credential list")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthServiceFinishWebAuthnLoginBranches(t *testing.T) {
|
|
t.Run("not configured", func(t *testing.T) {
|
|
svc := NewAuthService(newMockAuthRepo(), newMockRoleRepo(), newMockTokenStore(), nil, AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "token", []byte("{}")); !errors.Is(err, ErrWebAuthnNotConfigured) {
|
|
t.Fatalf("expected not configured, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid challenge", func(t *testing.T) {
|
|
svc := newConfiguredWebAuthnService(newWebAuthnRepoWithErrors(), newMockTokenStore())
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "missing", []byte("{}")); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("challenge invalid user id length", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
putWebAuthnChallengeToken(t, tokens, "baduid", webAuthnCeremonyLogin, []byte("short"), time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "baduid", []byte("{}")); !errors.Is(err, ErrWebAuthnChallengeInvalid) {
|
|
t.Fatalf("expected invalid challenge, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user fetch error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.getByIDErr = errors.New("db failed")
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
putWebAuthnChallengeToken(t, tokens, "tok1", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok1", []byte("{}")); !errors.Is(err, repo.getByIDErr) {
|
|
t.Fatalf("expected db error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("user not found", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
putWebAuthnChallengeToken(t, tokens, "tok2", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok2", []byte("{}")); !errors.Is(err, ErrInvalidCredentials) {
|
|
t.Fatalf("expected invalid credentials, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("load credentials error", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
repo.listCredsErr = errors.New("list creds failed")
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
putWebAuthnChallengeToken(t, tokens, "tok3", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok3", []byte("{}")); !errors.Is(err, repo.listCredsErr) {
|
|
t.Fatalf("expected list creds error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("no credentials", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
putWebAuthnChallengeToken(t, tokens, "tok4", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok4", []byte("{}")); !errors.Is(err, ErrWebAuthnCredentialUnavailable) {
|
|
t.Fatalf("expected credential unavailable, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid payload", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
repo.users[string(userID)] = &auth.User{ID: userID, Email: "user@example.com", IsActive: true}
|
|
serialized, err := json.Marshal(webauthnlib.Credential{ID: []byte{1}, PublicKey: []byte{2}})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
putWebAuthnChallengeToken(t, tokens, "tok5", webAuthnCeremonyLogin, userID, time.Now().UTC().Add(time.Minute))
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), "tok5", []byte("{}")); !errors.Is(err, ErrWebAuthnPayloadInvalid) {
|
|
t.Fatalf("expected payload invalid, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("verification failed", func(t *testing.T) {
|
|
repo := newWebAuthnRepoWithErrors()
|
|
tokens := newMockTokenStore()
|
|
userID := uuidv7.MustBytes()
|
|
now := time.Now().UTC()
|
|
user := &auth.User{
|
|
ID: userID,
|
|
Email: "user@example.com",
|
|
EmailVerifiedAt: &now,
|
|
FirstName: "Web",
|
|
LastName: "User",
|
|
IsActive: true,
|
|
}
|
|
repo.usersByMail["user@example.com"] = user
|
|
repo.users[string(userID)] = user
|
|
serialized, err := json.Marshal(webauthnlib.Credential{
|
|
ID: []byte{1, 2, 3},
|
|
PublicKey: []byte{4, 5, 6},
|
|
AttestationType: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal credential: %v", err)
|
|
}
|
|
repo.webauthnCredentials[string(userID)] = []auth.UserWebAuthnCredential{{
|
|
UserID: userID,
|
|
CredentialID: []byte{1, 2, 3},
|
|
CredentialJSON: serialized,
|
|
}}
|
|
|
|
svc := newConfiguredWebAuthnService(repo, tokens)
|
|
_, challengeToken, err := svc.BeginWebAuthnLogin(context.Background(), "user@example.com")
|
|
if err != nil {
|
|
t.Fatalf("begin login: %v", err)
|
|
}
|
|
if _, err := svc.FinishWebAuthnLogin(context.Background(), challengeToken, []byte(testCredentialAssertionResponseJSON)); !errors.Is(err, ErrWebAuthnVerificationFailed) {
|
|
t.Fatalf("expected verification failed, got %v", err)
|
|
}
|
|
})
|
|
}
|