mitbringsl/backend/internal/auth/password_test.go
Tronax 7b1c18590e
Backend Phase B (1/2): password auth + sessions
Argon2id password hashing (PHC format, self-encoded/decoded without an
external lib) with constant-time verification, UserStore (create/get by
email and id) and SessionStore (opaque crypto/rand tokens, SHA-256 hashed
in DB, create/lookup/revoke, last_seen_at bump on lookup).

HTTP layer: Register/Login/Logout handlers + RequireAuth middleware.
Login uses a dummy-hash path so unknown-email and wrong-password yield the
same timing/shape, narrowing user enumeration. Tokens accepted via Bearer
header (native clients) or session cookie (HttpOnly, SameSite=Lax).

Routes wired in api.go: POST /auth/register, /auth/login, /auth/logout.
Verified with go test, go vet and an end-to-end smoke test against a real
PostgreSQL container (register/login/logout/duplicate/short-pw/wrong-pw).

OIDC (Phase B part 2) follows next; the issueSession helper is reused.
2026-08-05 19:05:07 +02:00

67 lines
1.8 KiB
Go

package auth
import (
"strings"
"testing"
)
// fastParams keeps the test fast while still exercising the full PHC path.
var fastParams = Argon2Params{Memory: 4 * 1024, Iterations: 1, Parallelism: 1, SaltLength: 16, KeyLength: 32}
func TestHashPassword_PHCFormat(t *testing.T) {
h, err := HashPassword("hunter2", fastParams)
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
if !strings.HasPrefix(h, "$argon2id$v=19$") {
t.Fatalf("unexpected PHC prefix: %s", h)
}
if strings.Count(h, "$") != 5 {
t.Fatalf("expected 5 '$' separators, got %q", h)
}
}
func TestVerifyPassword_RoundTrip(t *testing.T) {
pw := "correct horse battery staple"
h, err := HashPassword(pw, fastParams)
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
if err := VerifyPassword(pw, h); err != nil {
t.Fatalf("VerifyPassword correct: %v", err)
}
if err := VerifyPassword("wrong", h); err == nil {
t.Fatal("VerifyPassword wrong: expected error, got nil")
}
}
func TestVerifyPassword_Malformed(t *testing.T) {
cases := []string{
"",
"not-a-hash",
"$argon2id$v=19$m=1024,t=1,p=1$AAAA$BB",
"$argon2i$v=19$m=1024,t=1,p=1$AAAA$BBBB",
"$argon2id$v=99$m=1024,t=1,p=1$AAAA$BBBB",
"$argon2id$v=19$m=0,t=1,p=1$AAAA$BBBB",
}
for _, c := range cases {
if err := VerifyPassword("x", c); err == nil {
t.Fatalf("VerifyPassword(%q): expected error", c)
}
}
}
func TestHashPassword_DifferentSalts(t *testing.T) {
a, _ := HashPassword("same", fastParams)
b, _ := HashPassword("same", fastParams)
if a == b {
t.Fatal("two hashes of the same password should differ due to random salt")
}
// both must still verify against the original password
if err := VerifyPassword("same", a); err != nil {
t.Fatalf("verify a: %v", err)
}
if err := VerifyPassword("same", b); err != nil {
t.Fatalf("verify b: %v", err)
}
}