From 4fecf73aba4c7925f4d6e21e044dfd31d45be37d Mon Sep 17 00:00:00 2001 From: pierrejeambrun Date: Thu, 21 May 2026 18:18:11 +0200 Subject: [PATCH] Revoke JWT on /auth/logout regardless of auth manager logout URL Previously, when an auth manager's get_url_logout() returned a URL, the /auth/logout endpoint short-circuited via early return and never invoked auth_manager.revoke_token(token_str). The JWT therefore remained valid after logout for auth managers like FabAuthManager and KeycloakAuthManager that redirect to an external logout URL. Move the revoke_token call before the early return so logout reliably invalidates the JWT token regardless of which auth manager is configured. --- .../core_api/routes/public/auth.py | 9 +++---- .../core_api/routes/public/test_auth.py | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) 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 f85bcec3a6123..6cf7529982cb6 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 @@ -57,14 +57,15 @@ def login(request: Request, auth_manager: AuthManagerDep, next: None | str = Non ) def logout(request: Request, auth_manager: AuthManagerDep) -> RedirectResponse: """Logout the user.""" + # Revoke the current token before any redirect or cookie deletion so the JWT + # is invalidated even when the auth manager redirects to an external logout URL. + if token_str := request.cookies.get(COOKIE_NAME_JWT_TOKEN): + auth_manager.revoke_token(token_str) + logout_url = auth_manager.get_url_logout() if logout_url: return RedirectResponse(logout_url) - # Revoke the current token before deleting the cookie - if token_str := request.cookies.get(COOKIE_NAME_JWT_TOKEN): - auth_manager.revoke_token(token_str) - secure = request.base_url.scheme == "https" or bool(conf.get("api", "ssl_cert", fallback="")) cookie_path = get_cookie_path() response = RedirectResponse(auth_manager.get_url_login()) 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 ca0c87acd8638..0379640f88799 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 @@ -220,3 +220,27 @@ def test_logout_without_cookie_does_not_revoke(self, logout_client): assert response.status_code == 307 assert RevokedToken.is_revoked("nonexistent-jti") is False + + def test_logout_revokes_token_when_logout_url_redirects(self, logout_client): + """Token must be revoked before the redirect when get_url_logout returns a URL.""" + now = int(time.time()) + auth_manager = logout_client.app.state.auth_manager + signer = auth_manager._get_token_signer() + token_payload = { + "sub": "admin", + "jti": "test-jti-redirect-456", + "exp": now + 3600, + "iat": now, + "nbf": now, + "aud": "apache-airflow", + "iss": signer.issuer, + } + token_str = jwt.encode(token_payload, signer._secret_key, algorithm=signer.algorithm) + + logout_client.cookies.set(COOKIE_NAME_JWT_TOKEN, token_str) + with patch.object(auth_manager, "get_url_logout", return_value="http://external/logout"): + response = logout_client.get("/auth/logout", follow_redirects=False) + + assert response.status_code == 307 + assert response.headers["location"] == "http://external/logout" + assert RevokedToken.is_revoked("test-jti-redirect-456") is True