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:
Tronax 2026-08-05 19:05:07 +02:00
parent 2899eb205b
commit 7b1c18590e
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
10 changed files with 756 additions and 29 deletions

View file

@ -5,24 +5,32 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/mitbringsl/backend/internal/auth"
"github.com/mitbringsl/backend/internal/config"
)
// API bundles all handler groups and wires the router.
// Handler groups are added in subsequent phases (auth, lists, items, suggestions).
// Handler groups are added in subsequent phases (lists, items, suggestions).
type API struct {
cfg *config.Config
pool *pgxpool.Pool
health *HealthHandler
auth *AuthHandler
}
// NewAPI constructs the API with all handler groups.
func NewAPI(cfg *config.Config, pool *pgxpool.Pool) *API {
users := auth.NewUserStore(pool)
sessions := auth.NewSessionStore(pool, auth.SessionConfig{
TokenBytes: cfg.SessionTokenBytes,
TTL: cfg.SessionTokenTTL,
})
return &API{
cfg: cfg,
pool: pool,
health: &HealthHandler{Pool: pool},
auth: NewAuthHandler(users, sessions, cfg),
}
}
@ -34,17 +42,17 @@ func (a *API) Handler() http.Handler {
mux.HandleFunc("GET /healthz", a.health.Healthz)
mux.HandleFunc("GET /readyz", a.health.Readyz)
// --- auth endpoints (added in Phase B) ---
// mux.HandleFunc("POST /auth/register", a.auth.Register)
// mux.HandleFunc("POST /auth/login", a.auth.Login)
// mux.HandleFunc("POST /auth/oidc", a.auth.OIDC)
// mux.HandleFunc("POST /auth/logout", a.auth.Logout)
// --- auth endpoints ---
mux.HandleFunc("POST /auth/register", a.auth.Register)
mux.HandleFunc("POST /auth/login", a.auth.Login)
mux.HandleFunc("POST /auth/logout", a.auth.Logout)
// mux.HandleFunc("POST /auth/oidc", a.auth.OIDC) // Phase B part 2
// --- authenticated API endpoints (added in Phase C) ---
// mux.HandleFunc("GET /api/lists", a.requireAuth(a.lists.List))
// mux.HandleFunc("GET /api/lists/{id}/ops", a.requireAuth(a.items.PullOps))
// mux.HandleFunc("POST /api/lists/{id}/ops", a.requireAuth(a.items.PushOps))
// mux.HandleFunc("GET /api/suggestions", a.requireAuth(a.suggest.Suggest))
// mux.HandleFunc("GET /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.List)))
// mux.HandleFunc("GET /api/lists/{id}/ops", a.RequireAuth(http.HandlerFunc(a.items.PullOps)))
// mux.HandleFunc("POST /api/lists/{id}/ops", a.RequireAuth(http.HandlerFunc(a.items.PushOps)))
// mux.HandleFunc("GET /api/suggestions", a.RequireAuth(http.HandlerFunc(a.suggest.Suggest)))
return Chain(
mux,