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.
This commit is contained in:
parent
2899eb205b
commit
7b1c18590e
10 changed files with 756 additions and 29 deletions
67
backend/internal/auth/password_test.go
Normal file
67
backend/internal/auth/password_test.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue