fix: block /auth/token for OAuth-only users without password_hash
Users without a password_hash (Google OAuth / magic-link accounts) could obtain a JWT by simply posting their email to /auth/token. Add an else clause that rejects such requests with 401, directing them to their configured auth provider. Update and extend tests accordingly.
This commit is contained in:
parent
55515266ea
commit
3205a8d300
3 changed files with 33 additions and 9 deletions
|
|
@ -38,7 +38,7 @@ async def create_token(
|
|||
request: TokenRequest,
|
||||
conn: duckdb.DuckDBPyConnection = Depends(_get_db),
|
||||
):
|
||||
"""Issue a JWT token. For dev/demo: any registered user gets a token."""
|
||||
"""Issue a JWT token. Requires password authentication."""
|
||||
repo = UserRepository(conn)
|
||||
user = repo.get_by_email(request.email)
|
||||
if not user:
|
||||
|
|
@ -54,6 +54,12 @@ async def create_token(
|
|||
ph.verify(user["password_hash"], request.password)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Invalid password")
|
||||
else:
|
||||
# No password set — must use their auth provider (Google OAuth, magic link)
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="This account uses external authentication. Please log in via your configured provider.",
|
||||
)
|
||||
|
||||
token = create_access_token(
|
||||
user_id=user["id"],
|
||||
|
|
|
|||
|
|
@ -64,10 +64,15 @@ class TestTokenEndpoint:
|
|||
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."""
|
||||
"""User without password_hash (OAuth-only) must be rejected at /auth/token."""
|
||||
resp = client.post("/auth/token", json={"email": "ml@test.com"})
|
||||
assert resp.status_code == 200
|
||||
assert "access_token" in resp.json()
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_token_rejected_for_oauth_only_user(self, client):
|
||||
"""OAuth-only user (no password_hash) must not receive a token via /auth/token."""
|
||||
resp = client.post("/auth/token", json={"email": "ml@test.com"})
|
||||
assert resp.status_code == 401
|
||||
assert "external authentication" in resp.json()["detail"]
|
||||
|
||||
def test_token_unknown_user_rejected(self, client):
|
||||
"""Unknown email must return 401."""
|
||||
|
|
|
|||
|
|
@ -64,18 +64,31 @@ class TestBootstrap:
|
|||
assert "already exist" in resp.json()["detail"]
|
||||
|
||||
def test_bootstrap_then_login(self, fresh_client):
|
||||
"""After bootstrap, normal /auth/token login works."""
|
||||
# Bootstrap
|
||||
"""After bootstrap with password, /auth/token login works; without password it requires OAuth."""
|
||||
# Bootstrap with a password
|
||||
fresh_client.post("/auth/bootstrap", json={
|
||||
"email": "admin@test.com",
|
||||
"password": "adminpass123",
|
||||
})
|
||||
|
||||
# Normal password login succeeds
|
||||
resp = fresh_client.post("/auth/token", json={
|
||||
"email": "admin@test.com",
|
||||
"password": "adminpass123",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["role"] == "admin"
|
||||
|
||||
def test_bootstrap_no_password_token_rejected(self, fresh_client):
|
||||
"""After passwordless bootstrap, /auth/token must reject the user (OAuth-only flow)."""
|
||||
fresh_client.post("/auth/bootstrap", json={
|
||||
"email": "admin@test.com",
|
||||
})
|
||||
|
||||
# Normal login
|
||||
resp = fresh_client.post("/auth/token", json={
|
||||
"email": "admin@test.com",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["role"] == "admin"
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_bootstrap_second_call_fails(self, fresh_client):
|
||||
"""Second bootstrap call fails — endpoint self-deactivates."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue