Skip to content

Implement FabAuthManager.refresh_user for sliding JWT refresh - #70980

Open
roshan-ramees wants to merge 1 commit into
apache:mainfrom
roshan-ramees:fab-refresh-user-sliding-jwt
Open

Implement FabAuthManager.refresh_user for sliding JWT refresh#70980
roshan-ramees wants to merge 1 commit into
apache:mainfrom
roshan-ramees:fab-refresh-user-sliding-jwt

Conversation

@roshan-ramees

Copy link
Copy Markdown

What?

Implements FabAuthManager.refresh_user so JWTRefreshMiddleware reissues the _token cookie on every request when the user is authenticated.

Why?

JWTRefreshMiddleware (added in #55506, 3.1.4) drives sliding-refresh of the UI JWT by calling auth_manager.refresh_user(user). BaseAuthManager.refresh_user is a no-op by default. KeycloakAuthManager overrides it — FabAuthManager does not. On FAB deployments (the default), the silent-refresh path does nothing, so long-open browser tabs burst 401s from concurrent /ui/* and /api/v2/* polls every [api_auth] jwt_expiration_time interval.

Symptom on our prod (Airflow 3.1.7 + FAB + Google OAuth):

[error] JWT token is not valid: Signature has expired
  [airflow.api_fastapi.auth.managers.base_auth_manager] loc=base_auth_manager.py:107
INFO: - "GET /ui/dashboard/dag_stats HTTP/1.1" 401 Unauthorized
INFO: - "GET /ui/dags?dag_runs_limit=14&limit=50&offset=0&exclude_stale=true&order_by=dag_display_name HTTP/1.1" 401 Unauthorized
INFO: - "GET /api/v2/dags/~/dagRuns/~/hitlDetails?state=deferred&response_received=false HTTP/1.1" 401 Unauthorized

Related open issue: #57065.

How?

Return the current user for authenticated (non-anonymous) FAB sessions. deserialize_user above already re-fetches the user from DB by token["sub"], so the object is fresh — returning it is safe and adds only one HS512 sign per request (sub-ms).

For anonymous / no user, return None to skip the reissue.

Tests

Three unit tests covering: authenticated user → returns user; anonymous user → returns None; no user → returns None.


^ Add meaningful description above
Read the Pull Request Guidelines for more information.
In case of fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
In case of a new dependency, check compliance with the ASF 3rd Party License Policy.
In case of backwards incompatible changes please leave a note in a newsfragment file, following instructions located in the newsfragments README.rst

@boring-cyborg

boring-cyborg Bot commented Aug 3, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

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#57065
@roshan-ramees
roshan-ramees force-pushed the fab-refresh-user-sliding-jwt branch from dcf2178 to e7edc61 Compare August 3, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant