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

@ -90,11 +90,18 @@ mitbringsl/
│ │ ├── config/config.go # Config (caarlos0/env)
│ │ ├── logging/logging.go # slog JSON-Setup
│ │ ├── store/db.go # pgxpool-Setup
│ │ ├── auth/ # PHASE B Password + Session
│ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword)
│ │ │ ├── password_test.go # PHC-Roundtrip-Tests
│ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID
│ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke
│ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505)
│ │ └── httpapi/
│ │ ├── api.go # API-Objekt + Router (Health aktiv, Rest noch auskommentiert)
│ │ ├── api.go # API-Objekt + Router (Health + /auth/* aktiv, Rest auskommentiert)
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
│ │ └── health.go # /healthz + /readyz
│ │ ├── health.go # /healthz + /readyz
│ │ └── auth.go # Register/Login/Logout-Handler + RequireAuth-Middleware
│ ├── migrations/
│ │ ├── embed.go # //go:embed *.sql
│ │ ├── 000001_init_schema.up.sql # users/sessions/lists/list_members/items/op_log/item_names
@ -123,8 +130,12 @@ Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
- ✅ **Phase A Docker:** Dockerfile (Go 1.26 → distroless nonroot), docker-compose
(caddy/backend/migrate/db), Caddyfile, `.env.example`.
**Verifiziert:** Image baut, beide Binaries laufen im Container (Smoke-Test OK).
- ⬜ **Phase B Auth:** Argon2id + eigene User (register/login) + Session-Middleware.
- ⬜ **Phase B OIDC:** go-oidc-Verifikation (Google + Generic) + `POST /auth/oidc`.
- ✅ **Phase B Auth (Password):** Argon2id im PHC-Format + `UserStore` (Create/GetByEmail/GetByID)
+ `SessionStore` (opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke) + Handler
`Register`/`Login`/`Logout` + `RequireAuth`-Middleware.
**Verifiziert:** `go test ./internal/auth/...` grün, E2E-Smoke-Test gegen echtes
PostgreSQL via Docker (Register/Login/Logout/Duplicate/Short-PW/Wrong-PW alle korrekt).
- 🚧 **Phase B OIDC:** go-oidc-Verifikation (Google + Generic) + `POST /auth/oidc`.
- ⬜ **Phase C Sync-Kern:** `op_log`-Append (idempotent), HLC, Projektion op→items/lists (LWW+Tombstones).
- ⬜ **Phase C Endpoints:** `/api/lists`, `/api/lists/{id}/ops` (push+pull).
- ⬜ **Phase C Suggestions:** `item_names`-Trigger + `/api/suggestions`.
@ -138,13 +149,23 @@ Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
- ⬜ **Phase F README + docs** (ARCHITECTURE/SYNC/API).
### Wo genau weitermachen?
**Nächster Schritt = Phase B (Auth):**
1. `internal/auth/password.go` Argon2id (PHC-Format) Hashen/Verifizieren.
2. `internal/auth/session.go` Token generieren (crypto/rand, base64url), SHA-256-Hash,
in `sessions` einfügen, Middleware `requireAuth` (lädt `user_id` in Context).
3. `internal/httpapi/auth.go` Handler `Register`/`Login`/`Logout`.
4. In `api.go` die `/auth/*`-Routen einkommentieren + verdrahten.
5. Dann Phase B Teil 2: OIDC (`internal/auth/oidc.go` + `POST /auth/oidc`).
**Nächster Schritt = Phase B Teil 2 (OIDC)** Teil 1 (Password-Auth) ist fertig ✅:
Teil 1 (erledigt):
- ✅ `internal/auth/password.go` Argon2id im PHC-Format (HashPassword/VerifyPassword).
- ✅ `internal/auth/user.go` UserStore (CreateUser/GetByEmail/GetByID).
- ✅ `internal/auth/session.go` SessionStore (crypto/rand + base64url + SHA-256, Create/Lookup/Revoke).
- ✅ `internal/httpapi/auth.go` Register/Login/Logout + RequireAuth-Middleware.
- ✅ In `api.go` sind `/auth/register`, `/auth/login`, `/auth/logout` aktiv verdrahtet.
Teil 2 (offen OIDC):
1. `internal/auth/oidc.go` Verifikation eines `id_token` via `github.com/coreos/go-oidc/v3`
(JWKS-Signatur, iss/aud/exp prüfen). Provider-Auswahl anhand `issuer` aus Config.
2. `POST /auth/oidc`-Handler in `internal/httpapi/auth.go`: empfängt `{provider, id_token}`,
verifiziert, findet/legt User an (oidc_issuer+oidc_subject UNIQUE), stellt Session aus
(gleicher `issueSession`-Pfad wie Login).
3. In `api.go` `mux.HandleFunc("POST /auth/oidc", a.auth.OIDC)` einkommentieren.
4. Config-Validierung: wenn `OIDC_GOOGLE_ENABLED=true`, muss `OIDC_GOOGLE_CLIENT_ID` gesetzt sein.
---