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.
67 lines
1.8 KiB
Go
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)
|
|
}
|
|
}
|