diff --git a/app/auth/router.py b/app/auth/router.py index a717ddc..2d7c4a1 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -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"], diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index be4f43f..182de73 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -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.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 4613ab4..07ea4d9 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -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."""