From e7edc6185c6f26c22b65a4f1e5264faebfb70790 Mon Sep 17 00:00:00 2001 From: Ramees Roshan Date: Mon, 3 Aug 2026 16:21:20 +0530 Subject: [PATCH] Implement FabAuthManager.refresh_user for sliding JWT refresh JWTRefreshMiddleware (added in 3.1.4) calls auth_manager.refresh_user on every request; if a user is returned it reissues the _token cookie so long-open UI tabs don't burst 401s at token expiry. BaseAuthManager.refresh_user is a no-op by default. KeycloakAuthManager overrides it; FAB does not, so on FAB deployments the silent-refresh path does nothing and UI tabs 401 every jwt_expiration_time interval. Return the current user unconditionally for authenticated FAB sessions. deserialize_user already re-fetches the user from DB by token['sub'], so the object is fresh and returning it is safe. Related: apache/airflow#57065 --- .../providers/fab/auth_manager/fab_auth_manager.py | 5 +++++ .../unit/fab/auth_manager/test_fab_auth_manager.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py index 09e584243020b..f488818c46e2c 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py @@ -315,6 +315,11 @@ def _fetch_user() -> User: def serialize_user(self, user: User) -> dict[str, Any]: return {"sub": str(user.id)} + def refresh_user(self, *, user: User) -> User | None: + if not user or user.is_anonymous: + return None + return user + def is_logged_in(self) -> bool: """Return whether the user is logged in.""" user = self.get_user() diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py index 62f9856701290..955029b1dfbdf 100644 --- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py +++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py @@ -246,6 +246,19 @@ def test_serialize_user(self, flask_app, auth_manager_with_appbuilder): result = auth_manager_with_appbuilder.serialize_user(user) assert result == {"sub": str(user.id)} + def test_refresh_user_returns_user_for_authenticated(self, auth_manager_with_appbuilder): + user = Mock() + user.is_anonymous = False + assert auth_manager_with_appbuilder.refresh_user(user=user) is user + + def test_refresh_user_returns_none_for_anonymous(self, auth_manager_with_appbuilder): + user = Mock() + user.is_anonymous = True + assert auth_manager_with_appbuilder.refresh_user(user=user) is None + + def test_refresh_user_returns_none_for_no_user(self, auth_manager_with_appbuilder): + assert auth_manager_with_appbuilder.refresh_user(user=None) is None + @mock.patch.object(FabAuthManager, "get_user") def test_is_logged_in(self, mock_get_user, auth_manager_with_appbuilder): user = Mock()