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.
75 lines
1.7 KiB
Go
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)
|
|
}
|
|
}
|