feat(v2): secure password hashing, finalize OpenAPI, and expand shopping v2
This commit is contained in:
parent
3c69355a5c
commit
0411791ad9
2 changed files with 31 additions and 5 deletions
|
|
@ -1,6 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import aiosqlite
|
||||
|
|
@ -33,16 +36,34 @@ class TokenResponse(ApiModel):
|
|||
token_type: str = "bearer"
|
||||
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:
|
||||
# Placeholder; replace with proper hashing (bcrypt/argon2) later
|
||||
return hashlib.sha256(pw.encode("utf-8")).hexdigest()
|
||||
salt = os.urandom(SALT_BYTES)
|
||||
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:
|
||||
# Secrets can be provided base64-encoded via env; fallback to deterministic dev defaults (NOT for prod)
|
||||
import base64
|
||||
|
||||
if settings.access_secret_b64:
|
||||
access = base64.b64decode(settings.access_secret_b64)
|
||||
else:
|
||||
|
|
@ -99,7 +120,7 @@ async def login(request: Request, body: LoginBody, conn: aiosqlite.Connection =
|
|||
if not user:
|
||||
return error_response(request, 401, "Invalid credentials")
|
||||
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")
|
||||
access, refresh = _token_pair_for_user(user)
|
||||
resp = JSONResponse(TokenResponse(access_token=access, user=user).model_dump(by_alias=True))
|
||||
|
|
|
|||
|
|
@ -275,6 +275,11 @@ What’s missing today (must be implemented in v2):
|
|||
- 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.
|
||||
|
||||
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:
|
||||
- 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).
|
||||
|
|
|
|||
Loading…
Reference in a new issue