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

vincbeck 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 4fa9816eef6 derive keycloak oauth redirect_uri from configured base 
url (#69801)
4fa9816eef6 is described below

commit 4fa9816eef64558a3178c0eb67ad52e3456a34d2
Author: Samina <[email protected]>
AuthorDate: Mon Jul 13 19:57:26 2026 +0530

    derive keycloak oauth redirect_uri from configured base url (#69801)
    
    The login and login_callback routes built the OAuth redirect_uri from the 
request host, so behind --proxy-headers (where the API server trusts all 
X-Forwarded-Host) a request header could steer the callback URL that Keycloak 
sends the authorization code to. Use the configured [api] base_url instead, 
matching the logout and post-login redirects in the same file.
---
 .../keycloak/auth_manager/routes/login.py          | 20 ++++++++-
 .../keycloak/auth_manager/routes/test_login.py     | 52 ++++++++++++++++++++++
 2 files changed, 70 insertions(+), 2 deletions(-)

diff --git 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/routes/login.py
 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/routes/login.py
index 9f2c80fe46a..58386b9aada 100644
--- 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/routes/login.py
+++ 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/routes/login.py
@@ -57,11 +57,27 @@ COOKIE_NAME_ID_TOKEN = "_id_token"
 COOKIE_NAME_OAUTH_STATE = "_oauth_state"
 
 
+def _login_callback_url(request: Request) -> str:
+    """
+    Build the OAuth ``redirect_uri`` for the login callback.
+
+    Prefer the configured public base URL (as ``logout`` and the post-login 
redirect
+    already do) so the value handed to Keycloak does not depend on the request
+    ``Host``/``X-Forwarded-Host`` headers, which are attacker-controlled when 
the API
+    server runs with ``--proxy-headers``. Fall back to the request URL when
+    ``base_url`` is not configured.
+    """
+    base_url = conf.get("api", "base_url", fallback=None)
+    if base_url:
+        return urljoin(base_url, 
f"{AUTH_MANAGER_FASTAPI_APP_PREFIX}/login_callback")
+    return str(request.url_for("login_callback"))
+
+
 @login_router.get("/login")
 def login(request: Request) -> RedirectResponse:
     """Initiate the authentication."""
     client = KeycloakAuthManager.get_keycloak_client()
-    redirect_uri = request.url_for("login_callback")
+    redirect_uri = _login_callback_url(request)
     state = secrets.token_urlsafe(32)
     auth_url = client.auth_url(redirect_uri=str(redirect_uri), scope="openid", 
state=state)
     response = RedirectResponse(auth_url)
@@ -85,7 +101,7 @@ def login_callback(request: Request):
         return HTMLResponse("Invalid OAuth state parameter", status_code=403)
 
     client = KeycloakAuthManager.get_keycloak_client()
-    redirect_uri = request.url_for("login_callback")
+    redirect_uri = _login_callback_url(request)
 
     tokens = client.token(
         grant_type="authorization_code",
diff --git 
a/providers/keycloak/tests/unit/keycloak/auth_manager/routes/test_login.py 
b/providers/keycloak/tests/unit/keycloak/auth_manager/routes/test_login.py
index 4f20715cc76..28323f184ef 100644
--- a/providers/keycloak/tests/unit/keycloak/auth_manager/routes/test_login.py
+++ b/providers/keycloak/tests/unit/keycloak/auth_manager/routes/test_login.py
@@ -22,6 +22,8 @@ import pytest
 
 from airflow.api_fastapi.app import AUTH_MANAGER_FASTAPI_APP_PREFIX
 
+from tests_common.test_utils.config import conf_vars
+
 
 class TestLoginRouter:
     
@patch("airflow.providers.keycloak.auth_manager.routes.login.KeycloakAuthManager.get_keycloak_client")
@@ -121,6 +123,56 @@ class TestLoginRouter:
         assert any("_token=" in c and "Secure" in c for c in set_cookies)
         assert any("_id_token=" in c and "Secure" in c for c in set_cookies)
 
+    @conf_vars({("api", "base_url"): "https://airflow.example.com"})
+    
@patch("airflow.providers.keycloak.auth_manager.routes.login.KeycloakAuthManager.get_keycloak_client")
+    def test_login_redirect_uri_uses_configured_base_url(self, 
mock_get_keycloak_client, client):
+        mock_keycloak_client = Mock()
+        mock_keycloak_client.auth_url.return_value = "redirect_url"
+        mock_get_keycloak_client.return_value = mock_keycloak_client
+        client.get(
+            "https://attacker.example.net"; + AUTH_MANAGER_FASTAPI_APP_PREFIX + 
"/login",
+            follow_redirects=False,
+        )
+        redirect_uri = 
mock_keycloak_client.auth_url.call_args.kwargs["redirect_uri"]
+        assert (
+            redirect_uri
+            == "https://airflow.example.com"; + AUTH_MANAGER_FASTAPI_APP_PREFIX 
+ "/login_callback"
+        )
+
+    @conf_vars({("api", "base_url"): "https://airflow.example.com"})
+    
@patch("airflow.providers.keycloak.auth_manager.routes.login.get_auth_manager")
+    
@patch("airflow.providers.keycloak.auth_manager.routes.login.KeycloakAuthManager.get_keycloak_client")
+    def test_login_callback_redirect_uri_uses_configured_base_url(
+        self, mock_get_keycloak_client, mock_get_auth_manager, client
+    ):
+        state = "state"
+        mock_keycloak_client = Mock()
+        mock_keycloak_client.token.return_value = {
+            "access_token": "access_token",
+            "refresh_token": "refresh_token",
+            "id_token": "id_token",
+        }
+        mock_keycloak_client.userinfo.return_value = {
+            "sub": "sub",
+            "preferred_username": "preferred_username",
+        }
+        mock_get_keycloak_client.return_value = mock_keycloak_client
+        mock_auth_manager = Mock()
+        mock_get_auth_manager.return_value = mock_auth_manager
+        mock_auth_manager.generate_jwt.return_value = "token"
+        client.get(
+            "https://attacker.example.net";
+            + AUTH_MANAGER_FASTAPI_APP_PREFIX
+            + f"/login_callback?code=code&state={state}",
+            follow_redirects=False,
+            cookies={"_oauth_state": state},
+        )
+        redirect_uri = 
mock_keycloak_client.token.call_args.kwargs["redirect_uri"]
+        assert (
+            redirect_uri
+            == "https://airflow.example.com"; + AUTH_MANAGER_FASTAPI_APP_PREFIX 
+ "/login_callback"
+        )
+
     def test_login_callback_without_code(self, client):
         response = client.get(AUTH_MANAGER_FASTAPI_APP_PREFIX + 
"/login_callback")
         assert response.status_code == 400

Reply via email to