mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-20 06:31:02 +08:00
fix(auth): fall back to session-based auth in _load_user (#14569)
## Summary Closes #13663. OAuth / OIDC callbacks call `login_user(user)` which writes `_user_id` into the session cookie, but `_load_user()` in `api/apps/__init__.py` only ever looked at the `Authorization` header. The SPA's response interceptor wipes the Authorization value from `localStorage` on the first 401 it sees — meaning that during the post-redirect window after an OAuth login, a single transient 401 sends every subsequent request back to the login page even though `login_user()` had already established a perfectly good server-side session. The reporter's analysis traces this all the way through the redirect → `navigate('/')` → first request → empty header → 401 → `removeAll()` → infinite-redirect-to-login chain. ## What changed - New `_load_user_from_session()` helper that reads `session["_user_id"]`, looks up the user in `UserService` (with the same `StatusEnum.VALID` and `access_token` checks already used elsewhere), and assigns `g.user`. - Every `return None` path in `_load_user()` now routes through that helper before giving up: - missing `Authorization` header - malformed `bearer ` prefix - empty / too-short JWT payload - JWT signature failure - JWT-resolved user not found / has no `access_token` - `APIToken.query()` fallback exhausted The JWT and API-token paths still take precedence — the session is only consulted when those can't authenticate the request. So existing local-login and SDK callers see no behaviour change; only OAuth / OIDC users that hit the original race now stay logged in. The Bearer-prefix issue called out in #13663 (lines 103-110) is already handled in the current code, so this PR only addresses the second half of the report. ## Test plan - [ ] Configure OIDC under `oauth` in `service_conf.yaml` - [ ] Click the OIDC login button, complete auth at the IdP - [ ] Confirm that navigating between pages no longer bounces back to `/login` - [ ] Confirm local email/password login still issues + accepts JWTs - [ ] Confirm SDK/API key callers still authenticate via `Authorization: Bearer <api-token>` --------- Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
This commit is contained in:
@@ -175,6 +175,96 @@ def test_load_user_api_token_fallback_and_fallback_exception(monkeypatch, caplog
|
||||
assert "api token fallback failed" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_load_user_session_fallback(monkeypatch, caplog):
|
||||
quart_app, apps_module = _load_apps_module(monkeypatch)
|
||||
|
||||
valid_token = "a" * 32
|
||||
valid_user = SimpleNamespace(id="user-1", email="oidc@example.com", access_token=valid_token)
|
||||
invalid_token_user = SimpleNamespace(id="user-1", email="oidc@example.com", access_token="INVALID_deadbeef")
|
||||
short_token_user = SimpleNamespace(id="user-1", email="oidc@example.com", access_token="too-short")
|
||||
|
||||
async def _case():
|
||||
# No Authorization header but a valid session: helper resolves the user.
|
||||
async with quart_app.test_request_context("/"):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
monkeypatch.setattr(apps_module.UserService, "query", lambda **_kw: [valid_user])
|
||||
assert apps_module._load_user() is valid_user
|
||||
|
||||
# Malformed bearer header still falls back to session.
|
||||
async with quart_app.test_request_context("/", headers={"Authorization": "Bearer"}):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
monkeypatch.setattr(apps_module.UserService, "query", lambda **_kw: [valid_user])
|
||||
assert apps_module._load_user() is valid_user
|
||||
|
||||
# Logout-revoked tokens (INVALID_ prefix) are rejected even with a session.
|
||||
async with quart_app.test_request_context("/"):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
monkeypatch.setattr(apps_module.UserService, "query", lambda **_kw: [invalid_token_user])
|
||||
assert apps_module._load_user() is None
|
||||
|
||||
# Short tokens are rejected (matches the JWT-path length floor).
|
||||
async with quart_app.test_request_context("/"):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
monkeypatch.setattr(apps_module.UserService, "query", lambda **_kw: [short_token_user])
|
||||
assert apps_module._load_user() is None
|
||||
|
||||
# No session and no header → still None.
|
||||
async with quart_app.test_request_context("/"):
|
||||
assert apps_module._load_user() is None
|
||||
|
||||
# Database errors during the session lookup are swallowed and logged.
|
||||
async with quart_app.test_request_context("/"):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
|
||||
def _raise(**_kw):
|
||||
raise RuntimeError("db down")
|
||||
|
||||
monkeypatch.setattr(apps_module.UserService, "query", _raise)
|
||||
with caplog.at_level(logging.ERROR):
|
||||
assert apps_module._load_user() is None
|
||||
|
||||
_run(_case())
|
||||
assert "load_user from session failed" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_load_user_session_fallback_after_token_paths_fail(monkeypatch):
|
||||
"""JWT-decode failures and API-token exhaustion must still fall through
|
||||
to the session and return the user, not None."""
|
||||
quart_app, apps_module = _load_apps_module(monkeypatch)
|
||||
|
||||
valid_token = "b" * 32
|
||||
valid_user = SimpleNamespace(id="user-1", email="oidc@example.com", access_token=valid_token)
|
||||
|
||||
def _raise_decode(_self, _auth):
|
||||
raise RuntimeError("jwt decode boom")
|
||||
|
||||
monkeypatch.setattr(apps_module.Serializer, "loads", _raise_decode)
|
||||
monkeypatch.setattr(apps_module.APIToken, "query", lambda **_kw: [])
|
||||
|
||||
async def _case():
|
||||
# JWT decode fails AND API-token query returns nothing → session wins.
|
||||
async with quart_app.test_request_context("/", headers={"Authorization": "Bearer junk"}):
|
||||
from quart import session
|
||||
|
||||
session["_user_id"] = "user-1"
|
||||
monkeypatch.setattr(apps_module.UserService, "query", lambda **_kw: [valid_user])
|
||||
assert apps_module._load_user() is valid_user
|
||||
|
||||
_run(_case())
|
||||
|
||||
|
||||
@pytest.mark.p2
|
||||
def test_login_required_timing_and_login_user_inactive(monkeypatch, caplog):
|
||||
quart_app, apps_module = _load_apps_module(monkeypatch)
|
||||
@@ -227,6 +317,7 @@ def test_logout_user_not_found_and_unauthorized_handlers(monkeypatch):
|
||||
assert "Not Found:" in payload["message"]
|
||||
|
||||
async with quart_app.test_request_context("/protected"):
|
||||
|
||||
@apps_module.login_required
|
||||
async def _protected():
|
||||
return {"ok": True}
|
||||
|
||||
Reference in New Issue
Block a user