Skip to content

Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware() - #70783

Open
stephen-bracken wants to merge 1 commit into
apache:mainfrom
stephen-bracken:make-jwt-extendible
Open

Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware()#70783
stephen-bracken wants to merge 1 commit into
apache:mainfrom
stephen-bracken:make-jwt-extendible

Conversation

@stephen-bracken

@stephen-bracken stephen-bracken commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

closes: #70720

Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware()

By moving the JWTRefreshMiddleware initialisation to BaseAuthManager.get_jwt_refresh_middleware(), this allows auth managers to override the JWTRefreshMiddleware behaviour by inheriting from it and using the inherited class in get_fastapi_middlewares instead.

By factoring out the _set_new_token() method from dispatch(), this gives an interface for the inherited token refresh middleware to alter the behaviour of the middleware when setting the tokens. The interface has access to the response object to set any cookies as needed.

Changed the _refresh_user() method to accept a request object to allow accessing any cookies or state information from the request.

Also adds the airflow.api_fastapi.app.request_cookie_is_secure() helper to standardise setting HTTP secure cookies

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
  • No

@stephen-bracken stephen-bracken changed the title Make JWTRefreshMiddleware into a standard fastapi middleware Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware()` Jul 30, 2026
@stephen-bracken stephen-bracken changed the title Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware()` Make JWTRefreshMiddleware extensible via BaseAuthManager.get_jwt_refresh_middleware() Jul 30, 2026
@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 3 times, most recently from 474ce7b to 7655032 Compare July 30, 2026 22:56
@stephen-bracken
stephen-bracken marked this pull request as ready for review July 30, 2026 23:38
Comment thread airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py Outdated
Comment thread airflow-core/src/airflow/api_fastapi/core_api/app.py Outdated
@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 3 times, most recently from 16198fd to 3a89840 Compare July 31, 2026 19:11
@vincbeck

Copy link
Copy Markdown
Contributor

CI is failing

@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 6 times, most recently from a8c2ae1 to 3dc4d12 Compare August 1, 2026 16:29
@stephen-bracken

stephen-bracken commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

I've updated _refresh_user to get the current_token from the request object, so now the request object is no longer a redundant argument.

edit: updated _set_new_token to generate the new jwt from the user, so the new_user argument is no longer redundant

@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 6 times, most recently from 87b198d to 908b21e Compare August 4, 2026 09:56
Comment thread airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py Outdated
@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 2 times, most recently from e94f3ba to a9bffa6 Compare August 12, 2026 14:42

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the PR focused around the target issue.

That's also updating/refactoring a whole bunch, making the PR harder to review and surface to test bigger.

  1. Moves generate_jwt from before call_next to inside _set_new_token after call_next — introduces the fail-hard-vs-fail-soft semantic
  2. Reorganizes the outer try scope so the "if current_token" gate lives inside _refresh_user — makes the flow harder to read for anyone tracing "when is _refresh_user called".
  3. Changes the outer condition from if new_token is not None to if new_user or new_token is not None. Under a minimal refactor, the original condition
    still works.
  4. Introduces the if new_user: else new_token = "" dead branch inside _set_new_token — dead because dispatch never calls it with new_user=None, but present because the extraction was over-scoped.
  5. Swaps delete_cookie for set_cookie(max_age=0) — drops the expires=0 attribute. Small, but again: not needed for extensibility.
  6. @classmethod async def on _set_new_token despite using neither cls nor await.

previous = flask_app.config.get("AUTH_ROLE_PUBLIC")
flask_app.config["AUTH_ROLE_PUBLIC"] = None
base_middleware = []
if AIRFLOW_V_3_4_PLUS:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why AIRFLOW_V_3_4_PLUS here?

@stephen-bracken stephen-bracken Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FabAuthManager change is for compatibility. I am assuming that this change will be released in 3.4.0 (as that is what airflow.__version__ is set to on main), which will add the changes to BaseAuthManager.get_fastapi_middlewares() and the _get_jwt_refresh_middleware() method - can correct the guard as necessary.

@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 5 times, most recently from 670f2da to 28c2f77 Compare August 12, 2026 16:54
@stephen-bracken

stephen-bracken commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@pierrejeambrun

  1. Moves generate_jwt from before call_next to inside _set_new_token after call_next — introduces the fail-hard-vs-fail-soft semantic

When call_next is called, the request.state.user attributes should be populated in the same way, allowing auth to take place via get_user(). If the user model was refreshed, the JWT cookies will be set on the response object from call_next like they normally would.

  1. Reorganizes the outer try scope so the "if current_token" gate lives inside _refresh_user — makes the flow harder to read for anyone tracing "when is _refresh_user called".

Restored the if current_token gate

  1. Changes the outer condition from if new_token is not None to if new_user or new_token is not None. Under a minimal refactor, the original condition
    still works.

In the new flow, new_token should only ever be None or "". The first condition checks whether we need to set any cookies and calls the necessary get_cookie_path and request_cookie_is_secure helpers, then clears the JWT cookie if new_token == "", or passes the user model and response to _set_new_token() to set the cookies if it is populated.

  1. Introduces the if new_user: else new_token = "" dead branch inside _set_new_token — dead because dispatch never calls it with new_user=None, but present because the extraction was over-scoped.

Removed dead branch

  1. Swaps delete_cookie for set_cookie(max_age=0) — drops the expires=0 attribute. Small, but again: not needed for extensibility.

Restored delete_cookie

  1. @classmethod async def on _set_new_token despite using neither cls nor await.

Swapped to @staticmethod async def to match _refresh_user()

@stephen-bracken
stephen-bracken force-pushed the make-jwt-extendible branch 2 times, most recently from 6e99a99 to d39ba4f Compare August 12, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make request cookies available to AuthManager methods

3 participants