Add user accounts: registration, login, and JWT auth
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

The first authenticated-user concept in Aurora Core -- everything else in
this codebase is public, read-only catalog data. Foundation for ratings
and a watchlist (not yet built): POST /auth/register, POST /auth/login,
GET /auth/me.

Security specifics worth knowing:
- Passwords hashed with argon2 (via argon2-cffi), never stored plain or
  logged.
- Login returns an identical 401 for a nonexistent email and a wrong
  password on a real one, including matching response timing
  (burn_time_like_a_password_check) -- otherwise either the status code
  or the timing alone lets a client enumerate registered emails.
- AURORA_SECRET_KEY now actually signs JWTs (previously defined but
  unused anywhere) -- .env.example's existing token_urlsafe(48) guidance
  already produces a long enough key, just flagged its new stakes.
- Email matched case-insensitively via explicit normalization in the
  service layer, not relied on from Pydantic's EmailStr (which does not
  lowercase the local part).

12 new integration tests (register/login/me over real HTTP via the
`client` fixture) plus 10 unit tests for the hashing/JWT logic itself,
following the same-day lesson that only unit tests actually execute
locally without Postgres -- the auth core is tested for real here rather
than only through integration tests that might not run before a deploy.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 06:15:39 -05:00
co-authored by Claude Sonnet 5
parent fde5e76bd7
commit 7bee596a0e
16 changed files with 628 additions and 2 deletions
+13
View File
@@ -20,6 +20,19 @@ a consistent envelope:
| GET | `/health` and `/api/v1/health` | Liveness only. |
| GET | `/ready` and `/api/v1/ready` | Checks PostgreSQL (always) and Redis (if `REDIS_REQUIRED=true`). |
## Accounts
| Method | Path | Body | Notes |
|---|---|---|---|
| POST | `/api/v1/auth/register` | `{"email", "password", "display_name"?}` | Creates an account and logs it in — returns an access token immediately, same as `/login`. `password` is 8-128 characters (length only; NIST 800-63B recommends against complexity rules). 409 if the email is already registered (matched case-insensitively). |
| POST | `/api/v1/auth/login` | `{"email", "password"}` | Returns `{"access_token", "token_type": "bearer"}`. A nonexistent email and a wrong password both return the identical 401 `invalid_credentials` — distinguishing them would let a client enumerate registered emails, including via response timing (see `app/core/auth.py:burn_time_like_a_password_check`). |
| GET | `/api/v1/auth/me` | — | Requires `Authorization: Bearer <access_token>`. Returns the caller's own `{"id", "email", "display_name", "created_at"}`. |
Access tokens are JWTs signed with `AURORA_SECRET_KEY`, valid for 24 hours,
with no refresh-token flow yet — a client re-authenticates (logs in again)
once a token expires. This is the foundation ratings and a watchlist will
build on next; those aren't live yet.
## Search
| Method | Path | Notes |