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

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


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 9b9dc13e87a [v3-1- test] Scope session token in cookie to base_url 
(#62771) (#62851)
9b9dc13e87a is described below

commit 9b9dc13e87a8847c49da229457d6de6c209b67ca
Author: Rahul Vats <[email protected]>
AuthorDate: Thu Mar 5 09:56:11 2026 +0530

    [v3-1- test] Scope session token in cookie to base_url (#62771) (#62851)
    
    * Scope session token in cookie to base_url (#62771)
    
    * Scope session token in cookie to base_url
    
    * Make get_cookie_path import backwards compatible
    
    (cherry picked from commit 43ee8c48c9bf6ec3de382052602a43afc4a0da34)
    
    * remove provider changes
    
    ---------
    
    Co-authored-by: Daniel Wolf <[email protected]>
---
 .../docs/core-concepts/auth-manager/index.rst      |  3 ++-
 airflow-core/src/airflow/api_fastapi/app.py        | 10 +++++++
 .../auth/managers/simple/routes/login.py           |  2 ++
 .../api_fastapi/auth/middlewares/refresh_token.py  |  3 ++-
 .../api_fastapi/core_api/routes/public/auth.py     |  2 ++
 .../auth/middlewares/test_refresh_token.py         | 31 ++++++++++++++++++++++
 .../core_api/routes/public/test_auth.py            | 15 +++++++++++
 airflow-core/tests/unit/api_fastapi/test_app.py    | 22 +++++++++++++++
 8 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/airflow-core/docs/core-concepts/auth-manager/index.rst 
b/airflow-core/docs/core-concepts/auth-manager/index.rst
index b2bcae33268..75c9244a111 100644
--- a/airflow-core/docs/core-concepts/auth-manager/index.rst
+++ b/airflow-core/docs/core-concepts/auth-manager/index.rst
@@ -160,12 +160,13 @@ cookie named ``_token`` before redirecting to the Airflow 
UI. The Airflow UI wil
 
 .. code-block:: python
 
+    from airflow.api_fastapi.app import get_cookie_path
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
COOKIE_NAME_JWT_TOKEN
 
     response = RedirectResponse(url="/")
 
     secure = request.base_url.scheme == "https" or bool(conf.get("api", 
"ssl_cert", fallback=""))
-    response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, secure=secure, 
httponly=True)
+    response.set_cookie(COOKIE_NAME_JWT_TOKEN, token, path=get_cookie_path(), 
secure=secure, httponly=True)
     return response
 
 .. note::
diff --git a/airflow-core/src/airflow/api_fastapi/app.py 
b/airflow-core/src/airflow/api_fastapi/app.py
index 7c05295807e..1720c9f08ec 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -49,6 +49,16 @@ API_ROOT_PATH = urlsplit(API_BASE_URL).path
 # Define the full path on which the potential auth manager fastapi is mounted
 AUTH_MANAGER_FASTAPI_APP_PREFIX = f"{API_ROOT_PATH}auth"
 
+
+def get_cookie_path() -> str:
+    """
+    Return the path to scope cookies to, derived from ``[api] base_url``.
+
+    Falls back to ``"/"`` when no ``base_url`` is configured.
+    """
+    return API_ROOT_PATH or "/"
+
+
 # Fast API apps mounted under these prefixes are not allowed
 RESERVED_URL_PREFIXES = ["/api/v2", "/ui", "/execution"]
 
diff --git 
a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py 
b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py
index 372aecf6035..55df83634ec 100644
--- a/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py
+++ b/airflow-core/src/airflow/api_fastapi/auth/managers/simple/routes/login.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 from fastapi import Depends, Request, status
 from starlette.responses import RedirectResponse
 
+from airflow.api_fastapi.app import get_cookie_path
 from airflow.api_fastapi.auth.managers.base_auth_manager import 
COOKIE_NAME_JWT_TOKEN
 from airflow.api_fastapi.auth.managers.simple.datamodels.login import 
LoginBody, LoginResponse
 from airflow.api_fastapi.auth.managers.simple.services.login import 
SimpleAuthManagerLogin
@@ -93,6 +94,7 @@ def login_all_admins(request: Request) -> RedirectResponse:
     response.set_cookie(
         COOKIE_NAME_JWT_TOKEN,
         SimpleAuthManagerLogin.create_token_all_admins(),
+        path=get_cookie_path(),
         secure=secure,
         httponly=True,
     )
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 a64da351d25..ac2a3d0dee5 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
@@ -21,7 +21,7 @@ from fastapi import HTTPException, Request
 from fastapi.responses import JSONResponse
 from starlette.middleware.base import BaseHTTPMiddleware
 
-from airflow.api_fastapi.app import get_auth_manager
+from airflow.api_fastapi.app import get_auth_manager, get_cookie_path
 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
@@ -65,6 +65,7 @@ class JWTRefreshMiddleware(BaseHTTPMiddleware):
                 response.set_cookie(
                     COOKIE_NAME_JWT_TOKEN,
                     new_token,
+                    path=get_cookie_path(),
                     httponly=True,
                     secure=secure,
                     samesite="lax",
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
index a97b7fd9972..8f4ed3d74b2 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/auth.py
@@ -19,6 +19,7 @@ from __future__ import annotations
 from fastapi import HTTPException, Request, status
 from fastapi.responses import RedirectResponse
 
+from airflow.api_fastapi.app import get_cookie_path
 from airflow.api_fastapi.auth.managers.base_auth_manager import 
COOKIE_NAME_JWT_TOKEN
 from airflow.api_fastapi.common.router import AirflowRouter
 from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
@@ -60,6 +61,7 @@ def logout(request: Request) -> RedirectResponse:
     response = RedirectResponse(auth_manager.get_url_login())
     response.delete_cookie(
         key=COOKIE_NAME_JWT_TOKEN,
+        path=get_cookie_path(),
         secure=secure,
         httponly=True,
     )
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 b8f0d7c7726..09943c2f6cf 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
@@ -130,3 +130,34 @@ class TestJWTRefreshMiddleware:
         mock_auth_manager.generate_jwt.assert_called_once_with(refreshed_user)
         set_cookie_headers = response.headers.get("set-cookie", "")
         assert f"{COOKIE_NAME_JWT_TOKEN}=new_token" in set_cookie_headers
+
+    
@patch("airflow.api_fastapi.auth.middlewares.refresh_token.get_cookie_path", 
return_value="/team-a/")
+    
@patch("airflow.api_fastapi.auth.middlewares.refresh_token.get_auth_manager")
+    
@patch("airflow.api_fastapi.auth.middlewares.refresh_token.resolve_user_from_token")
+    @patch("airflow.api_fastapi.auth.middlewares.refresh_token.conf")
+    @pytest.mark.asyncio
+    async def test_dispatch_cookie_uses_subpath(
+        self,
+        mock_conf,
+        mock_resolve_user_from_token,
+        mock_get_auth_manager,
+        mock_cookie_path,
+        middleware,
+        mock_request,
+        mock_user,
+    ):
+        """When a subpath is configured, set_cookie must include it as 
path=."""
+        refreshed_user = MagicMock(spec=BaseUser)
+        mock_request.cookies = {COOKIE_NAME_JWT_TOKEN: "valid_token"}
+        mock_resolve_user_from_token.return_value = mock_user
+        mock_auth_manager = MagicMock()
+        mock_get_auth_manager.return_value = mock_auth_manager
+        mock_auth_manager.refresh_user.return_value = refreshed_user
+        mock_auth_manager.generate_jwt.return_value = "new_token"
+        mock_conf.get.return_value = ""
+
+        call_next = AsyncMock(return_value=Response())
+        response = await middleware.dispatch(mock_request, call_next)
+
+        set_cookie_headers = response.headers.get("set-cookie", "")
+        assert "Path=/team-a/" in set_cookie_headers
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 d4a5e5869e3..14b30845a4a 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
@@ -20,10 +20,13 @@ from unittest.mock import MagicMock, patch
 
 import pytest
 
+from airflow.api_fastapi.auth.managers.base_auth_manager import 
COOKIE_NAME_JWT_TOKEN
+
 from tests_common.test_utils.config import conf_vars
 
 AUTH_MANAGER_LOGIN_URL = "http://some_login_url";
 AUTH_MANAGER_LOGOUT_URL = "http://some_logout_url";
+SUBPATH = "/team-a/"
 
 pytestmark = pytest.mark.db_test
 
@@ -94,3 +97,15 @@ class TestLogout(TestAuthEndpoint):
 
         assert response.status_code == 307
         assert response.headers["location"] == expected_redirection
+
+    @patch("airflow.api_fastapi.core_api.routes.public.auth.get_cookie_path", 
return_value=SUBPATH)
+    def test_logout_cookie_uses_subpath(self, mock_cookie_path, test_client):
+        """Cookies must use the subpath so they are scoped to the correct 
instance."""
+        test_client.app.state.auth_manager.get_url_logout.return_value = None
+
+        response = test_client.get("/auth/logout", follow_redirects=False)
+
+        assert response.status_code == 307
+        cookies = response.headers.get_list("set-cookie")
+        token_cookie = next(c for c in cookies if f"{COOKIE_NAME_JWT_TOKEN}=" 
in c)
+        assert f"Path={SUBPATH}" in token_cookie
diff --git a/airflow-core/tests/unit/api_fastapi/test_app.py 
b/airflow-core/tests/unit/api_fastapi/test_app.py
index 448d527ab6b..cd43260211b 100644
--- a/airflow-core/tests/unit/api_fastapi/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/test_app.py
@@ -118,3 +118,25 @@ def test_plugin_with_invalid_url_prefix(caplog, 
fastapi_apps, expected_message,
 
     assert any(expected_message in rec.message for rec in caplog.records)
     assert not any(r.path == invalid_path for r in app.routes)
+
+
+class TestGetCookiePath:
+    def test_default_returns_slash(self):
+        """When no base_url is configured, get_cookie_path() should return 
'/'."""
+        with mock.patch.object(app_module, "API_ROOT_PATH", "/"):
+            assert app_module.get_cookie_path() == "/"
+
+    def test_empty_returns_slash(self):
+        """When API_ROOT_PATH is empty, get_cookie_path() should return '/'."""
+        with mock.patch.object(app_module, "API_ROOT_PATH", ""):
+            assert app_module.get_cookie_path() == "/"
+
+    def test_subpath(self):
+        """When base_url contains a subpath, get_cookie_path() should return 
it."""
+        with mock.patch.object(app_module, "API_ROOT_PATH", "/team-a/"):
+            assert app_module.get_cookie_path() == "/team-a/"
+
+    def test_nested_subpath(self):
+        """When base_url contains a nested subpath, get_cookie_path() should 
return it."""
+        with mock.patch.object(app_module, "API_ROOT_PATH", 
"/org/team-a/airflow/"):
+            assert app_module.get_cookie_path() == "/org/team-a/airflow/"

Reply via email to