feat(v2): secure password hashing, finalize OpenAPI, and expand shopping v2

This commit is contained in:
jableader 2025-11-01 15:02:37 +11:00
parent 3c69355a5c
commit 0411791ad9
2 changed files with 31 additions and 5 deletions

View file

@ -1,6 +1,9 @@
from __future__ import annotations from __future__ import annotations
import base64
import hashlib import hashlib
import hmac
import os
from typing import Optional from typing import Optional
import aiosqlite import aiosqlite
@ -33,16 +36,34 @@ class TokenResponse(ApiModel):
token_type: str = "bearer" token_type: str = "bearer"
user: User user: User
PBKDF2_ALG = "pbkdf2_sha256"
PBKDF2_ITER = 390000 # similar to Django default; adjust in settings if needed
SALT_BYTES = 16
def _hash_pw(pw: str) -> str: def _hash_pw(pw: str) -> str:
# Placeholder; replace with proper hashing (bcrypt/argon2) later salt = os.urandom(SALT_BYTES)
return hashlib.sha256(pw.encode("utf-8")).hexdigest() dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, PBKDF2_ITER)
return f"{PBKDF2_ALG}${PBKDF2_ITER}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
def _verify_pw(pw: str, stored: str) -> bool:
try:
alg, iter_s, salt_b64, hash_b64 = stored.split("$", 3)
if alg != PBKDF2_ALG:
return False
iters = int(iter_s)
salt = base64.b64decode(salt_b64)
expected = base64.b64decode(hash_b64)
dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, iters)
# constant-time compare
return hmac.compare_digest(dk, expected)
except Exception:
return False
def _jwt_config() -> JwtConfig: def _jwt_config() -> JwtConfig:
# Secrets can be provided base64-encoded via env; fallback to deterministic dev defaults (NOT for prod) # Secrets can be provided base64-encoded via env; fallback to deterministic dev defaults (NOT for prod)
import base64
if settings.access_secret_b64: if settings.access_secret_b64:
access = base64.b64decode(settings.access_secret_b64) access = base64.b64decode(settings.access_secret_b64)
else: else:
@ -99,7 +120,7 @@ async def login(request: Request, body: LoginBody, conn: aiosqlite.Connection =
if not user: if not user:
return error_response(request, 401, "Invalid credentials") return error_response(request, 401, "Invalid credentials")
stored = await users_db.get_local_password_hash(conn, user.id) stored = await users_db.get_local_password_hash(conn, user.id)
if not stored or stored != _hash_pw(body.password): if not stored or not _verify_pw(body.password, stored):
return error_response(request, 401, "Invalid credentials") return error_response(request, 401, "Invalid credentials")
access, refresh = _token_pair_for_user(user) access, refresh = _token_pair_for_user(user)
resp = JSONResponse(TokenResponse(access_token=access, user=user).model_dump(by_alias=True)) resp = JSONResponse(TokenResponse(access_token=access, user=user).model_dump(by_alias=True))

View file

@ -275,6 +275,11 @@ Whats missing today (must be implemented in v2):
- Household scoping: path prefixes, membership checks, repository filtering by `household_id` across all data tables. - Household scoping: path prefixes, membership checks, repository filtering by `household_id` across all data tables.
- OpenAPI security scheme update to JWT bearer; `403` responses for membership violations. - OpenAPI security scheme update to JWT bearer; `403` responses for membership violations.
Immediate priorities (feedback-incorporated):
- Upgrade password hashing from SHA-256 to a secure, adaptive scheme (PBKDF2/bcrypt/argon2). DONE with PBKDF2; consider migrating to argon2 in future.
- Finalize and commit `openapi.json` with household-scoped routes to unblock frontend (temporary X-Household-Slug header can be removed).
- Plan v1 cleanup: once parity is achieved for scoped routes, rename `*_v2.py` to canonical filenames and remove original v1 modules to avoid long-term duplication.
What to preserve from v1: What to preserve from v1:
- CamelCase response keys, non-null collection properties, `Page<T>` envelope and cursor semantics, RFC7807 responses, `Location` header on create. - CamelCase response keys, non-null collection properties, `Page<T>` envelope and cursor semantics, RFC7807 responses, `Location` header on create.
- Shopping outward storeName normalization ("home" instead of empty string). - Shopping outward storeName normalization ("home" instead of empty string).