fix: require password verification when user has password_hash in /auth/token

Previously the password check was gated on both user.password_hash and
request.password being truthy, so an attacker could omit the password
field (which defaults to "") and receive a valid JWT. Now any user with a
stored hash must supply a non-empty password that passes argon2 verification.

Adds six TestTokenEndpoint tests covering empty, missing, wrong, and correct
password, plus no-hash user and unknown user cases.
This commit is contained in:
ZdenekSrotyr 2026-04-09 06:44:31 +02:00
parent 89154d043b
commit 94c6b0f839
2 changed files with 42 additions and 2 deletions

View file

@ -44,8 +44,10 @@ async def create_token(
if not user:
raise HTTPException(status_code=401, detail="User not found")
# If user has password_hash, verify it
if user.get("password_hash") and request.password:
# If user has password_hash, require and verify it
if user.get("password_hash"):
if not request.password:
raise HTTPException(status_code=401, detail="Password required")
try:
from argon2 import PasswordHasher
ph = PasswordHasher()

View file

@ -37,6 +37,44 @@ def client(tmp_path):
return TestClient(app)
class TestTokenEndpoint:
"""Tests for /auth/token — password bypass fix."""
def test_token_empty_password_rejected_when_user_has_hash(self, client):
"""Empty password must be rejected when user has password_hash."""
resp = client.post("/auth/token", json={"email": "pw@test.com", "password": ""})
assert resp.status_code == 401
def test_token_missing_password_rejected_when_user_has_hash(self, client):
"""Omitting password field (defaults to '') must be rejected when user has password_hash."""
resp = client.post("/auth/token", json={"email": "pw@test.com"})
assert resp.status_code == 401
def test_token_wrong_password_rejected(self, client):
"""Wrong password must be rejected with 401."""
resp = client.post("/auth/token", json={"email": "pw@test.com", "password": "wrongpass"})
assert resp.status_code == 401
def test_token_correct_password_succeeds(self, client):
"""Correct password must issue a token."""
resp = client.post("/auth/token", json={"email": "pw@test.com", "password": "testpass123"})
assert resp.status_code == 200
data = resp.json()
assert "access_token" in data
assert data["email"] == "pw@test.com"
def test_token_no_password_hash_user_gets_token(self, client):
"""User without password_hash (e.g. OAuth-only user) still gets a token without a password."""
resp = client.post("/auth/token", json={"email": "ml@test.com"})
assert resp.status_code == 200
assert "access_token" in resp.json()
def test_token_unknown_user_rejected(self, client):
"""Unknown email must return 401."""
resp = client.post("/auth/token", json={"email": "nobody@test.com", "password": "anything"})
assert resp.status_code == 401
class TestPasswordAuth:
def test_login_success(self, client):
resp = client.post("/auth/password/login", json={