This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit ac324964dea01cbb51a2e536bbbeab1cb4339aee
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Fri Nov 6 09:28:45 2026 +0100

    [v3-4-test] Make `JWTRefreshMiddleware` extensible via 
`BaseAuthManager.get_jwt_refresh_middleware()` (#75868)
    
    * [v3-4-test] Keep release-candidate provider pins until each is released 
or yanked (#75919) (#75924)
    
    Constraints for an Airflow release candidate pin the provider wave being
    voted on at its rc versions, and commit them onto the shared
    constraints-X-Y branch. The next regular generation for the vX-Y-test
    branch resolves without pre-releases, so it saw the older finals as a
    provider downgrade and failed - the v3-4-test canary has been red since
    3.4.0rc1 for exactly that reason, although nothing was yanked.
    
    The final release has the opposite gap: it ignored the candidates, so a
    provider rc that was neither released nor yanked was silently passed
    over and the released constraints never recorded that decision. It now
    has to be made, and the run says which providers it is still waiting on.
    
    The alert that should have reported the canary failure never reached
    Slack because the action was not told which API method to call, and the
    twice-daily CI notification announced "All passing" because it read a
    run that was still in progress, whose missing conclusion looked like no
    failures.
    (cherry picked from commit eeae004bd25e02c696f619f61ab67a3fb671c81c)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
    
    * Make JWTRefreshMiddleware extensible via 
BaseAuthManager.get_jwt_refresh_middleware() (#70783)
    
    Co-authored-by: Stephen Bracken <email-protected>
    (cherry picked from commit e5d8cbbe67931dd13a027ed3386ddcc1877800cd)
    
    ---------
    
    Co-authored-by: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
    Co-authored-by: Jarek Potiuk <[email protected]>
    Co-authored-by: stephen-bracken 
<[email protected]>
---
 airflow-core/src/airflow/api_fastapi/app.py        | 18 +++++
 .../api_fastapi/auth/managers/base_auth_manager.py | 14 ++++
 .../api_fastapi/auth/middlewares/refresh_token.py  | 77 +++++++++++++++-----
 .../src/airflow/api_fastapi/core_api/app.py        |  8 ++-
 .../auth/managers/test_base_auth_manager.py        | 10 +++
 .../auth/middlewares/test_refresh_token.py         | 81 +++++++++++++++++++---
 .../core_api/routes/public/test_auth.py            |  4 +-
 .../tests/unit/api_fastapi/core_api/test_app.py    | 29 ++++++++
 airflow-core/tests/unit/api_fastapi/test_app.py    | 16 +++++
 9 files changed, 226 insertions(+), 31 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/app.py 
b/airflow-core/src/airflow/api_fastapi/app.py
index bf8f28b4b5d..f52d34a4fde 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -44,6 +44,8 @@ from airflow.exceptions import AirflowConfigException
 from airflow.utils.providers_configuration_loader import 
providers_configuration_loaded
 
 if TYPE_CHECKING:
+    from fastapi import Request
+
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
BaseAuthManager
 
 API_BASE_URL = conf.get("api", "base_url", fallback="")
@@ -64,6 +66,22 @@ def get_cookie_path() -> str:
     return API_ROOT_PATH or "/"
 
 
+def request_cookie_is_secure(request: Request) -> bool:
+    """
+    Return whether a FastAPI request cookie should be tagged as HTTP secure.
+
+    :param request: FastAPI Request object
+
+    usage:
+    ```python
+    secure = request_cookie_is_secure(request)
+
+    response.set_cookie("mycookie", "myvalue", path=get_cookie_path(), 
secure=secure, httponly=True)
+    ```
+    """
+    return request.base_url.scheme == "https" or bool(conf.get("api", 
"ssl_cert", fallback=""))
+
+
 # Fast API apps mounted under these prefixes are not allowed
 RESERVED_URL_PREFIXES = ["/api/v2", "/ui", "/execution", "/auth", "/pluginsv2"]
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py 
b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
index 94b96ba5af2..8b2302393a0 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/managers/base_auth_manager.py
@@ -185,6 +185,20 @@ class BaseAuthManager(Generic[T], LoggingMixin, 
metaclass=ABCMeta):
         """
         return []
 
+    def get_jwt_refresh_middleware(self) -> 
list[tuple[_MiddlewareFactory[Any], dict[str, Any]]]:
+        """
+        Return the JWTRefreshMiddleware to refresh the Airflow JWT token.
+
+        Middlewares added by this method will be initialised before middleware 
from
+        ``get_fastapi_middlewares()``. To disable JWT refresh return an empty 
list.
+        To add more middleware, simply add more tuples here in the same format 
as
+        ``get_fastapi_middlewares()``.
+        """
+        from airflow.api_fastapi.auth.middlewares.refresh_token import 
JWTRefreshMiddleware
+        from airflow.api_fastapi.core_api.app import _STATIC_URL_PATH
+
+        return [(JWTRefreshMiddleware, {"skip_path_prefixes": 
(f"{_STATIC_URL_PATH}/",)})]
+
     def generate_jwt(
         self, user: T, *, expiration_time_in_seconds: int = 
conf.getint("api_auth", "jwt_expiration_time")
     ) -> str:
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py 
b/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py
index 37186d9be53..01aa96ffe79 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/middlewares/refresh_token.py
@@ -24,19 +24,19 @@ from fastapi.responses import JSONResponse
 from starlette.concurrency import run_in_threadpool
 from starlette.datastructures import MutableHeaders
 
-from airflow.api_fastapi.app import get_auth_manager, get_cookie_path
+from airflow.api_fastapi.app import get_auth_manager, get_cookie_path, 
request_cookie_is_secure
 from airflow.api_fastapi.auth.managers.base_auth_manager import 
COOKIE_NAME_JWT_TOKEN
 from airflow.api_fastapi.auth.managers.exceptions import 
AuthManagerRefreshTokenExpiredException
-from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
 from airflow.api_fastapi.core_api.security import (
     USER_INJECTED_BY_TRUSTED_MIDDLEWARE,
     resolve_user_from_token,
 )
-from airflow.configuration import conf
 
 if TYPE_CHECKING:
     from starlette.types import ASGIApp, Message, Receive, Scope, Send
 
+    from airflow.api_fastapi.auth.managers.models.base_user import BaseUser
+
 # Unauthenticated endpoints that never read the user, so resolving it there 
would only cost a
 # JWT decode and a revocation lookup on every health check and UI boot.
 UNAUTHENTICATED_PATHS = frozenset(["/api/v2/monitor/health", 
"/api/v2/version"])
@@ -72,27 +72,29 @@ class JWTRefreshMiddleware:
             await self.app(scope, receive, send)
             return
 
-        new_token = None
+        new_user = None
+        session_invalidated = False
         try:
-            new_user, current_user = await self._refresh_user(current_token)
+            new_user, current_user = await self.refresh_user(request)
             if user := (new_user or current_user):
                 # Stamp the trust sentinel alongside the user so `get_user()`
                 # can distinguish this trusted assignment from a stray write
                 # by unrelated middleware.
                 request.state.user = user
                 request.state.user_authenticated_via = 
USER_INJECTED_BY_TRUSTED_MIDDLEWARE
-            if new_user:
-                # If we created a new user, serialize it and set it as a cookie
-                new_token = get_auth_manager().generate_jwt(new_user)
         except (HTTPException, AuthManagerRefreshTokenExpiredException):
             # Receive a HTTPException when the Airflow token is expired
             # Receive a AuthManagerRefreshTokenExpiredException when the 
potential underlying refresh
             # token used by the auth manager is expired
-            new_token = ""
+            session_invalidated = True
 
-        token_cookie, cleanup_cookies = (
-            self._build_cookie_headers(request, new_token) if new_token is not 
None else (None, [])
-        )
+        if session_invalidated or new_user is not None:
+            extra_cookies = self.get_extra_cookies(request, new_user)
+            token_cookie, cleanup_cookies = self.build_cookie_headers(
+                request, await self.generate_new_user_token(request, 
new_user), extra_cookies
+            )
+        else:
+            token_cookie, cleanup_cookies = (None, [])
         response_started = False
 
         async def send_with_cookies(message: Message) -> None:
@@ -124,10 +126,34 @@ class JWTRefreshMiddleware:
         return path in UNAUTHENTICATED_PATHS or 
path.startswith(self.skip_path_prefixes)
 
     @staticmethod
-    def _build_cookie_headers(request: Request, new_token: str) -> tuple[str, 
list[str]]:
-        """Return the ``Set-Cookie`` value of the token and those clearing 
stale token cookies."""
+    def get_extra_cookies(request: Request, new_user: BaseUser | None) -> 
dict[str, str]:
+        """
+        Get extra cookies from the request context to set in a new token 
response.
+
+        Setting a cookie to "" will cause it to be cleared by 
build_cookie_headers()
+        """
+        return {}
+
+    @staticmethod
+    async def generate_new_user_token(request: Request, new_user: BaseUser | 
None) -> str:
+        """Generate a new Airflow JWT token from the user model."""
+        return get_auth_manager().generate_jwt(new_user) if new_user else ""
+
+    @staticmethod
+    def build_cookie_headers(
+        request: Request, new_token: str, extra_cookies: dict[str, str]
+    ) -> tuple[str, list[str]]:
+        """
+        Return the ``Set-Cookie`` value of the token and those clearing stale 
token cookies.
+
+        Any cookies with a nil value ("") will be cleared with Max-Age=0
+
+        :param request: FastAPI Request
+        :param new_token: New Airflow JWT token
+        :param extra_cookies: Additional cookie values to set
+        """
         cookie_path = get_cookie_path()
-        secure = request.base_url.scheme == "https" or bool(conf.get("api", 
"ssl_cert", fallback=""))
+        secure = request_cookie_is_secure(request)
         # Only used to render the Set-Cookie header values
         response = Response()
         response.set_cookie(
@@ -151,13 +177,32 @@ class JWTRefreshMiddleware:
                 secure=secure,
                 samesite="lax",
             )
+        # Add any additional cookies required
+        for cookie, value in extra_cookies.items():
+            response.set_cookie(
+                cookie,
+                value,
+                path=cookie_path,
+                httponly=True,
+                secure=secure,
+                samesite="lax",
+                max_age=0 if value == "" else None,
+            )
         token_cookie, *cleanup_cookies = (
             value.decode("latin-1") for key, value in response.raw_headers if 
key == b"set-cookie"
         )
         return token_cookie, cleanup_cookies
 
     @staticmethod
-    async def _refresh_user(current_token: str) -> tuple[BaseUser | None, 
BaseUser | None]:
+    async def refresh_user(request: Request) -> tuple[BaseUser | None, 
BaseUser | None]:
+        """
+        Refresh the logged in user using the current JWT Token.
+
+        If the user is not authenticated, return ``None, None``
+
+        :param request: FastAPI Request
+        """
+        current_token = request.cookies.get(COOKIE_NAME_JWT_TOKEN)
         user = await resolve_user_from_token(current_token)
         # Auth managers may do blocking I/O here (e.g. calling an identity 
provider)
         return await run_in_threadpool(get_auth_manager().refresh_user, 
user=user), user
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py 
b/airflow-core/src/airflow/api_fastapi/core_api/app.py
index 02f7151ab55..ccd1c6745dd 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/app.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py
@@ -304,11 +304,13 @@ def init_config(app: FastAPI) -> None:
 
 def init_middlewares(app: FastAPI) -> None:
     from airflow.api_fastapi.app import get_auth_manager
-    from airflow.api_fastapi.auth.middlewares.refresh_token import 
JWTRefreshMiddleware
 
-    app.add_middleware(JWTRefreshMiddleware, 
skip_path_prefixes=(f"{_STATIC_URL_PATH}/",))
+    auth_manager = get_auth_manager()
 
-    for middleware_cls, middleware_kwargs in 
get_auth_manager().get_fastapi_middlewares():
+    for middleware_cls, middleware_kwargs in (
+        *auth_manager.get_jwt_refresh_middleware(),
+        *auth_manager.get_fastapi_middlewares(),
+    ):
         app.add_middleware(middleware_cls, **middleware_kwargs)
 
     # GZipMiddleware must be inside HttpAccessLogMiddleware so that access 
logs capture
diff --git 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
index 18a9ceeecd2..e433c1223da 100644
--- 
a/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
+++ 
b/airflow-core/tests/unit/api_fastapi/auth/managers/test_base_auth_manager.py
@@ -36,8 +36,10 @@ from 
airflow.api_fastapi.auth.managers.models.resource_details import (
     TeamDetails,
     VariableDetails,
 )
+from airflow.api_fastapi.auth.middlewares.refresh_token import 
JWTRefreshMiddleware
 from airflow.api_fastapi.auth.tokens import JWTGenerator, JWTValidator
 from airflow.api_fastapi.common.types import MenuItem
+from airflow.api_fastapi.core_api.app import _STATIC_URL_PATH
 from airflow.exceptions import RemovedInAirflow4Warning
 from airflow.models.pool import Pool
 from airflow.models.team import Team
@@ -932,3 +934,11 @@ class TestBaseAuthManager:
         user = BaseAuthManagerUserTest(name=user_id)
         result = 
auth_manager.is_authorized_hitl_task(assigned_users=assigned_users, user=user)
         assert result == expected
+
+    def test_get_jwt_refresh_middleware(self, auth_manager):
+        assert auth_manager.get_jwt_refresh_middleware() == [
+            (JWTRefreshMiddleware, {"skip_path_prefixes": 
(f"{_STATIC_URL_PATH}/",)})
+        ]
+
+    def test_get_fastapi_middlewares(self, auth_manager):
+        assert auth_manager.get_fastapi_middlewares() == []
diff --git 
a/airflow-core/tests/unit/api_fastapi/auth/middlewares/test_refresh_token.py 
b/airflow-core/tests/unit/api_fastapi/auth/middlewares/test_refresh_token.py
index a601a0417d1..39aa7216b9d 100644
--- a/airflow-core/tests/unit/api_fastapi/auth/middlewares/test_refresh_token.py
+++ b/airflow-core/tests/unit/api_fastapi/auth/middlewares/test_refresh_token.py
@@ -32,6 +32,8 @@ from airflow.api_fastapi.auth.middlewares import refresh_token
 from airflow.api_fastapi.auth.middlewares.refresh_token import 
JWTRefreshMiddleware
 from airflow.api_fastapi.core_api.security import 
USER_INJECTED_BY_TRUSTED_MIDDLEWARE
 
+from tests_common.test_utils.config import conf_vars
+
 MODULE = "airflow.api_fastapi.auth.middlewares.refresh_token"
 
 
@@ -97,9 +99,8 @@ def mock_resolve_user():
 
 @pytest.fixture(autouse=True)
 def no_ssl_cert():
-    with patch(f"{MODULE}.conf", autospec=True) as mock_conf:
-        mock_conf.get.return_value = ""
-        yield mock_conf
+    with conf_vars({("api", "ssl_cert"): ""}):
+        yield
 
 
 class TestJWTRefreshMiddleware:
@@ -272,18 +273,18 @@ class TestJWTRefreshMiddleware:
         ],
     )
     def test_cookie_secure_flag(
-        self, mock_resolve_user, mock_auth_manager, no_ssl_cert, scheme, 
ssl_cert, expected_secure
+        self, mock_resolve_user, mock_auth_manager, scheme, ssl_cert, 
expected_secure
     ):
         """The cookie Secure flag must follow the request scheme as well as 
the local ssl_cert."""
         mock_auth_manager.refresh_user.return_value = MagicMock(spec=BaseUser)
         mock_auth_manager.generate_jwt.return_value = "new_token"
-        no_ssl_cert.get.return_value = ssl_cert
-        app, _ = _build_app()
-        client = TestClient(
-            app, base_url=f"{scheme}://testserver", 
cookies={COOKIE_NAME_JWT_TOKEN: "valid_token"}
-        )
+        with conf_vars({("api", "ssl_cert"): ssl_cert}):
+            app, _ = _build_app()
+            client = TestClient(
+                app, base_url=f"{scheme}://testserver", 
cookies={COOKIE_NAME_JWT_TOKEN: "valid_token"}
+            )
 
-        response = client.get("/api/v2/dags")
+            response = client.get("/api/v2/dags")
 
         assert ("Secure" in response.headers["set-cookie"]) is expected_secure
 
@@ -314,3 +315,63 @@ class TestJWTRefreshMiddleware:
         assert any(
             "Path=/" in h and "Path=/team-a/" not in h and "Max-Age=0" in h 
for h in set_cookie_headers
         )
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize("new_user", [(MagicMock(),), (None,)])
+    async def test_generate_new_user_token(self, mock_auth_manager, new_user):
+        mock_auth_manager.generate_jwt.return_value = "new_token"
+        app, seen = _build_app()
+        middleware = JWTRefreshMiddleware(app)
+        new_token = await middleware.generate_new_user_token(MagicMock(), 
new_user)
+        if new_user is None:
+            assert new_token == ""
+        else:
+            assert new_token == "new_token"
+
+    @patch(f"{MODULE}.JWTRefreshMiddleware.get_extra_cookies")
+    def test_refresh_user_with_extra_cookies(
+        self, mock_get_extra_cookies: MagicMock, mock_resolve_user, 
mock_auth_manager
+    ):
+        mock_get_extra_cookies.return_value = {"foo": "bar"}
+        refreshed_user = MagicMock(spec=BaseUser)
+        mock_auth_manager.refresh_user.return_value = refreshed_user
+        mock_auth_manager.generate_jwt.return_value = "new_token"
+        app, seen = _build_app()
+
+        response = TestClient(app, cookies={COOKIE_NAME_JWT_TOKEN: 
"valid_token"}).get("/api/v2/dags")
+
+        # JWTRefreshMiddleware must stamp the trust sentinel so `get_user()` 
accepts
+        # the cached user. Without the marker, the auth dependency falls 
through
+        # to a fresh JWT validation, defeating the refresh.
+        assert seen["user"] is refreshed_user
+        assert seen["authenticated_via"] is USER_INJECTED_BY_TRUSTED_MIDDLEWARE
+        
mock_auth_manager.refresh_user.assert_called_once_with(user=mock_resolve_user.return_value)
+        mock_auth_manager.generate_jwt.assert_called_once_with(refreshed_user)
+        assert (
+            response.headers["set-cookie"]
+            == "_token=new_token; HttpOnly; Path=/; SameSite=lax, foo=bar; 
HttpOnly; Path=/; SameSite=lax"
+        )
+
+    @patch(f"{MODULE}.JWTRefreshMiddleware.get_extra_cookies")
+    def test_refresh_user_clear_extra_cookies(
+        self, mock_get_extra_cookies: MagicMock, mock_resolve_user, 
mock_auth_manager
+    ):
+        mock_get_extra_cookies.return_value = {"foo": ""}
+        refreshed_user = MagicMock(spec=BaseUser)
+        mock_auth_manager.refresh_user.return_value = refreshed_user
+        mock_auth_manager.generate_jwt.return_value = "new_token"
+        app, seen = _build_app()
+
+        response = TestClient(app, cookies={COOKIE_NAME_JWT_TOKEN: 
"valid_token"}).get("/api/v2/dags")
+
+        # JWTRefreshMiddleware must stamp the trust sentinel so `get_user()` 
accepts
+        # the cached user. Without the marker, the auth dependency falls 
through
+        # to a fresh JWT validation, defeating the refresh.
+        assert seen["user"] is refreshed_user
+        assert seen["authenticated_via"] is USER_INJECTED_BY_TRUSTED_MIDDLEWARE
+        
mock_auth_manager.refresh_user.assert_called_once_with(user=mock_resolve_user.return_value)
+        mock_auth_manager.generate_jwt.assert_called_once_with(refreshed_user)
+        assert (
+            response.headers["set-cookie"]
+            == '_token=new_token; HttpOnly; Path=/; SameSite=lax, foo=""; 
HttpOnly; Max-Age=0; Path=/; SameSite=lax'
+        )
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
index fc4e84621e4..6fd0143388f 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_auth.py
@@ -367,7 +367,7 @@ class TestLogoutTokenRevocation:
         # The refresh middleware is stubbed out so that only the logout route 
can clear the cookie.
         with (
             patch(
-                
"airflow.api_fastapi.auth.middlewares.refresh_token.JWTRefreshMiddleware._refresh_user",
+                
"airflow.api_fastapi.auth.middlewares.refresh_token.JWTRefreshMiddleware.refresh_user",
                 new=AsyncMock(return_value=(None, None)),
             ),
             patch.object(auth_manager, "get_url_logout", 
return_value="http://external/logout";),
@@ -389,7 +389,7 @@ class TestLogoutTokenRevocation:
         logout_client.cookies.set(COOKIE_NAME_JWT_TOKEN, token_str)
         with (
             patch(
-                
"airflow.api_fastapi.auth.middlewares.refresh_token.JWTRefreshMiddleware._refresh_user",
+                
"airflow.api_fastapi.auth.middlewares.refresh_token.JWTRefreshMiddleware.refresh_user",
                 new=AsyncMock(return_value=(None, None)),
             ),
             patch.object(auth_manager, "get_url_logout", 
return_value="http://external/logout";),
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py 
b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
index 3d26d632639..ae765987760 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/test_app.py
@@ -20,10 +20,12 @@ import contextlib
 import inspect
 import json
 import typing
+from unittest.mock import MagicMock, Mock, patch
 
 import pytest
 from fastapi import FastAPI, Request
 from fastapi.middleware.cors import CORSMiddleware
+from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.params import Depends as DependsClass
 from fastapi.responses import StreamingResponse
 from fastapi.templating import Jinja2Templates
@@ -31,9 +33,11 @@ from starlette.routing import Mount
 from starlette.testclient import TestClient
 
 from airflow.api_fastapi.app import create_app
+from airflow.api_fastapi.auth.managers.base_auth_manager import BaseAuthManager
 from airflow.api_fastapi.core_api.app import (
     base_react_app_bundle_urls,
     init_config,
+    init_middlewares,
     init_ui_translation_views,
 )
 from airflow.api_fastapi.core_api.routes.public import authenticated_router
@@ -384,3 +388,28 @@ class TestBaseReactAppPreload:
         html = self._render("index.html", ['/x.js" onload="alert(1)'])
 
         assert 'onload="alert(1)"' not in html
+
+
+class TestMiddlewareInitialisation:
+    @patch("airflow.api_fastapi.app.get_auth_manager")
+    def test_jwt_refresh_middleware_init(self, mock_get_auth_manager):
+        app = FastAPI()
+        auth_manager = MagicMock(spec=BaseAuthManager)
+        mock_jwt_middleware = Mock()
+        auth_manager.get_jwt_refresh_middleware.return_value = 
[(mock_jwt_middleware, {})]
+        auth_manager.get_fastapi_middlewares.return_value = []
+        mock_get_auth_manager.return_value = auth_manager
+        init_middlewares(app)
+        auth_manager.get_jwt_refresh_middleware.assert_called_once()
+        assert [m.cls for m in app.user_middleware] == [GZipMiddleware, 
mock_jwt_middleware]
+
+    @patch("airflow.api_fastapi.app.get_auth_manager")
+    def test_no_jwt_refresh_middleware_init(self, mock_get_auth_manager):
+        app = FastAPI()
+        auth_manager = MagicMock(spec=BaseAuthManager)
+        auth_manager.get_jwt_refresh_middleware.return_value = []
+        auth_manager.get_fastapi_middlewares.return_value = []
+        mock_get_auth_manager.return_value = auth_manager
+        init_middlewares(app)
+        auth_manager.get_jwt_refresh_middleware.assert_called_once()
+        assert [m.cls for m in app.user_middleware] == [GZipMiddleware]
diff --git a/airflow-core/tests/unit/api_fastapi/test_app.py 
b/airflow-core/tests/unit/api_fastapi/test_app.py
index 82e0674bafe..e0fb816c8c5 100644
--- a/airflow-core/tests/unit/api_fastapi/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/test_app.py
@@ -347,6 +347,22 @@ class TestGetCookiePath:
             assert app_module.get_cookie_path() == "/org/team-a/airflow/"
 
 
[email protected](
+    ("scheme", "ssl_cert", "exp"),
+    [
+        ("http", "", False),
+        ("https", "", True),
+        ("http", "/foo/bar/cert.crt", True),
+        ("https", "/foo/bar/cert/crt", True),
+    ],
+)
+def test_request_cookie_is_secure(scheme, ssl_cert, exp):
+    request = mock.Mock()
+    request.base_url.scheme = scheme
+    with conf_vars({("api", "ssl_cert"): ssl_cert}):
+        assert app_module.request_cookie_is_secure(request) == exp
+
+
 def test_create_auth_manager_thread_safety():
     """Concurrent calls to create_auth_manager must return the same singleton 
instance."""
     call_count = 0

Reply via email to