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