require_auth now uses HTTPBearer(auto_error=False) and raises 401 explicitly
when the Authorization header is missing or non-Bearer, matching
require_auth_401. Unauthenticated requests are 401 for every user endpoint;
authenticated-but-forbidden (cross-entity / role) stays 403.
- app/auth_middleware.py: single optional bearer, 401 on missing token
(require_auth_401 kept as a compatibility alias)
- tests: update 4 pre-existing cases that asserted 403 without a token
(test_auth, test_reports, test_kpi_causality, test_expenses,
test_api_auth_surface_20260914) - intentional behavior change
- add tests/test_auth_401_unification_20260915.py: sample + full sweep
(all AUTHED GET routes -> 401), invalid token -> 401, cross-entity and
role-denied still 403, bot channel (X-BOT-KEY) and public paths unchanged
Root cause: main.py include_router had no global auth dependency; user routers
(products/cost/predict/...) relied on assert_entity_scope, which treats a
missing session token as the Bot service channel and lets the request through.
Result: 46 user endpoints returned data without any token on a public site.
Changes:
- auth_middleware.py: add require_auth_401 (missing Bearer -> 401, delegates to
require_auth otherwise); 401/403 semantics are explicit.
- router-level Depends(require_auth_401) on products, cost, predict,
notifications, permissions, tenant, alignment; endpoint-level on
/auth/roles and /admin/cache/clear.
- deps.py: document that get_entity_id/assert_entity_scope are NOT auth.
- frontend: add the missing Bearer header to 5 tokenless fetch/axios calls that
hit the newly protected endpoints (KPIAlignment x2, RolePermissions x2,
Dashboard x1).
- tests: new test_api_auth_surface_20260914.py (58 cases: no-token 401,
token 200, cross-entity 403, bot channel unchanged); adapt test_predict /
test_kpi_forecast to send session tokens like production.
- scripts/audit_api_auth_surface.py: reusable FastAPI dependency-graph audit
(423 endpoints; A bot / B gap / C public), exits 1 when a B-class gap exists.