This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new e5d8cbbe679 Make JWTRefreshMiddleware extensible via
BaseAuthManager.get_jwt_refresh_middleware() (#70783)
e5d8cbbe679 is described below
commit e5d8cbbe67931dd13a027ed3386ddcc1877800cd
Author: stephen-bracken <[email protected]>
AuthorDate: Thu Oct 22 15:11:02 2026 +0100
Make JWTRefreshMiddleware extensible via
BaseAuthManager.get_jwt_refresh_middleware() (#70783)
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 49255373024..3c2c94114a5 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -45,6 +45,8 @@ from airflow.observability.metrics import stats_utils
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="")
@@ -65,6 +67,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 7b2985746e1..836ba405610 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 HttpMetricsMiddleware and
HttpAccessLogMiddleware so that both
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 c5d5e33222f..149179ebd62 100644
--- a/airflow-core/tests/unit/api_fastapi/test_app.py
+++ b/airflow-core/tests/unit/api_fastapi/test_app.py
@@ -345,6 +345,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