This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authsqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gamertan.com/web/access"
|
||||
"gamertan.com/web/account"
|
||||
"gamertan.com/web/auth"
|
||||
"gamertan.com/web/authwebauthn"
|
||||
)
|
||||
|
||||
func TestAccountRegistrationCommitsEveryRequiredArtifact(t *testing.T) {
|
||||
store, authService, accountService, passkeys := accountFixture(t, true)
|
||||
started, err := accountService.Start(t.Context(), account.StartInput{Email: "PERSON@example.test", Username: "person.one", DisplayName: "Person One", Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if started.User.Email != "person@example.test" || !started.User.RegistrationPending {
|
||||
t.Fatalf("pending user=%+v", started.User)
|
||||
}
|
||||
if _, err = authService.VerifyPassword(t.Context(), started.User.Email, "correct horse battery staple"); !errors.Is(err, auth.ErrInactiveUser) {
|
||||
t.Fatalf("pending password verification err=%v", err)
|
||||
}
|
||||
if _, err = accountService.BeginPasskey(t.Context(), started.RegistrationToken, "Primary passkey"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
finished, err := accountService.FinishWithPasskey(t.Context(), started.RegistrationToken, "ceremony-token", []byte(`{"id":"fixture"}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if finished.User.RegistrationPending || finished.User.ID != started.User.ID || len(finished.RecoveryCodes) != 10 || finished.SessionToken == "" || !finished.Organization.Personal {
|
||||
t.Fatalf("finish=%+v code-count=%d", finished, len(finished.RecoveryCodes))
|
||||
}
|
||||
if passkeys.userID != started.User.ID || passkeys.binding != started.RegistrationToken {
|
||||
t.Fatalf("passkey binding user=%q binding=%q", passkeys.userID, passkeys.binding)
|
||||
}
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_passkey_credentials WHERE user_id=?`, started.User.ID, 1)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_recovery_codes WHERE user_id=?`, started.User.ID, 10)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_organizations WHERE personal_owner_user_id=?`, started.User.ID, 1)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_access_bindings WHERE subject_id=? AND role_name='owner'`, started.User.ID, 1)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_account_registrations WHERE user_id=?`, started.User.ID, 0)
|
||||
if _, err = authService.VerifyPassword(t.Context(), started.User.Email, "correct horse battery staple"); err != nil {
|
||||
t.Fatalf("completed password verification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordAccountCanFinishWithoutPasskey(t *testing.T) {
|
||||
store, authService, accountService, _ := accountFixture(t, true)
|
||||
started, err := accountService.Start(t.Context(), account.StartInput{Email: "reader@example.test", Username: "reader.one", DisplayName: "Reader One", Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
finished, err := accountService.FinishPassword(t.Context(), started.RegistrationToken)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if finished.SessionToken == "" || len(finished.RecoveryCodes) != 10 || len(finished.PasskeyCredential.ID) != 0 {
|
||||
t.Fatalf("password finish=%+v code-count=%d", finished, len(finished.RecoveryCodes))
|
||||
}
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_passkey_credentials WHERE user_id=?`, started.User.ID, 0)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_recovery_codes WHERE user_id=?`, started.User.ID, 10)
|
||||
if _, err = authService.VerifyPassword(t.Context(), "reader@example.test", "correct horse battery staple"); err != nil {
|
||||
t.Fatalf("password account not active: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountRegistrationRollsBackWhenOwnerPolicyIsMissing(t *testing.T) {
|
||||
store, authService, accountService, _ := accountFixture(t, false)
|
||||
started, err := accountService.Start(t.Context(), account.StartInput{Email: "rollback@example.test", Username: "rollback.one", DisplayName: "Rollback One", Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = accountService.BeginPasskey(t.Context(), started.RegistrationToken, "Primary passkey"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = accountService.FinishWithPasskey(t.Context(), started.RegistrationToken, "ceremony-token", []byte(`{"id":"fixture"}`)); err == nil {
|
||||
t.Fatal("completion unexpectedly succeeded without seeded owner role")
|
||||
}
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_passkey_credentials WHERE user_id=?`, started.User.ID, 0)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_recovery_codes WHERE user_id=?`, started.User.ID, 0)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_organizations WHERE personal_owner_user_id=?`, started.User.ID, 0)
|
||||
assertCount(t, store, `SELECT COUNT(*) FROM gwf_account_registrations WHERE user_id=?`, started.User.ID, 1)
|
||||
if _, err = authService.VerifyPassword(t.Context(), started.User.Email, "correct horse battery staple"); !errors.Is(err, auth.ErrInactiveUser) {
|
||||
t.Fatalf("rolled-back account became usable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func accountFixture(t *testing.T, seedOwner bool) (*Store, *auth.Service, *account.Service, *accountPasskeys) {
|
||||
t.Helper()
|
||||
store, err := Open(filepath.Join(t.TempDir(), "identity.sqlite"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
if seedOwner {
|
||||
err = store.SeedAccessPolicy(t.Context(), access.Policy{
|
||||
Roles: map[string]string{"owner": "Personal organization owner"},
|
||||
Permissions: map[string]string{"account.view": "View the account"},
|
||||
Grants: map[string][]string{"owner": {"account.view"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
authService, err := auth.New(store, auth.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
passkeys := &accountPasskeys{now: time.Now().UTC()}
|
||||
accountService, err := account.New(store, passkeys, authService, account.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return store, authService, accountService, passkeys
|
||||
}
|
||||
|
||||
type accountPasskeys struct {
|
||||
userID, binding string
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (passkeys *accountPasskeys) BeginAccountRegistration(_ context.Context, userID, _ string, binding []byte) (authwebauthn.BeginResult, error) {
|
||||
passkeys.userID = userID
|
||||
passkeys.binding = string(binding)
|
||||
return authwebauthn.BeginResult{CeremonyToken: "ceremony-token", PublicKey: []byte(`{}`), ExpiresAt: passkeys.now.Add(5 * time.Minute)}, nil
|
||||
}
|
||||
|
||||
func (passkeys *accountPasskeys) FinishAccountRegistration(ctx context.Context, ceremonyToken string, binding, _ []byte, commit authwebauthn.RegistrationCommit) (authwebauthn.Credential, error) {
|
||||
if ceremonyToken != "ceremony-token" || string(binding) != passkeys.binding {
|
||||
return authwebauthn.Credential{}, authwebauthn.ErrOperationBinding
|
||||
}
|
||||
credential := authwebauthn.Credential{ID: []byte("fixture-credential-id"), UserID: passkeys.userID, Label: "Primary passkey", Data: []byte(`{"id":"fixture-credential-id"}`), CreatedAt: passkeys.now}
|
||||
audit := auth.AuditEvent{ID: "passkey-audit-id", ActorUserID: passkeys.userID, Action: "auth.account.passkey", ResourceType: "passkey", ResourceID: "fixture-credential-id", Summary: "The initial account passkey was enrolled.", CreatedAt: passkeys.now}
|
||||
if err := commit(ctx, credential, audit); err != nil {
|
||||
return authwebauthn.Credential{}, err
|
||||
}
|
||||
return credential, nil
|
||||
}
|
||||
|
||||
func assertCount(t *testing.T, store *Store, query, id string, want int) {
|
||||
t.Helper()
|
||||
var got int
|
||||
if err := store.db.QueryRow(query, id).Scan(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("count for %q = %d, want %d", query, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user