Vellum/internal/auth/password_test.go
mithral 9772e55c73 Phase 1: accounts, sessions, per-library grants
Multi-user from the first migration, as planned.

- auth: argon2id password hashing (PHC encoded, parameters travel with the
  hash), opaque session tokens stored only as SHA-256, login rate limiting,
  and account enumeration resistance (unknown user hashes anyway)
- sessions: cookie or Bearer auth, HttpOnly + SameSite=Lax, revocation on
  password change
- grants: library_grants is the single access-control chokepoint. Admins see
  every library; everyone else sees exactly their can_read grants
- bootstrap: VELLUM_ADMIN_USER/PASSWORD create the first admin only on an
  empty database. No default password is ever invented.
- httpapi: mounts modules on a shared mux, session middleware innermost
- httpx: shared JSON helpers, strict body decoding

Gate: two users see different libraries. Tests cover that, plus session
expiry, token-at-rest, grant upsert, and the full login/me/logout flow.
2026-10-09 16:31:24 -04:00

75 lines
1.7 KiB
Go

package auth
import (
"errors"
"strings"
"testing"
)
func TestHashAndVerify(t *testing.T) {
const pw = "correct horse battery staple"
encoded, err := HashPassword(pw)
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
if !strings.HasPrefix(encoded, "$argon2id$") {
t.Errorf("hash %q is not an argon2id PHC string", encoded)
}
ok, err := VerifyPassword(pw, encoded)
if err != nil {
t.Fatalf("VerifyPassword: %v", err)
}
if !ok {
t.Error("correct password did not verify")
}
ok, err = VerifyPassword("wrong password", encoded)
if err != nil {
t.Fatalf("VerifyPassword(wrong): %v", err)
}
if ok {
t.Error("wrong password verified")
}
}
func TestHashIsSalted(t *testing.T) {
a, err := HashPassword("same password")
if err != nil {
t.Fatal(err)
}
b, err := HashPassword("same password")
if err != nil {
t.Fatal(err)
}
if a == b {
t.Error("two hashes of the same password are identical; salt is not random")
}
}
func TestVerifyRejectsMalformedHash(t *testing.T) {
for _, bad := range []string{
"",
"not-a-hash",
"$argon2id$v=19$m=65536,t=2,p=4$onlyonefield",
"$bcrypt$v=19$m=65536,t=2,p=4$c2FsdA$aGFzaA",
} {
if _, err := VerifyPassword("x", bad); err == nil {
t.Errorf("VerifyPassword(%q) succeeded, want error", bad)
} else if !errors.Is(err, ErrInvalidHash) {
t.Errorf("VerifyPassword(%q) error = %v, want ErrInvalidHash", bad, err)
}
}
}
func TestVerifyRejectsFutureArgonVersion(t *testing.T) {
h, err := HashPassword("x")
if err != nil {
t.Fatal(err)
}
bumped := strings.Replace(h, "v=19", "v=20", 1)
if _, err := VerifyPassword("x", bumped); !errors.Is(err, ErrIncompatibleVersion) {
t.Errorf("error = %v, want ErrIncompatibleVersion", err)
}
}