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 2c98ac79d93 Return 403 when Keycloak UMA ticket grant returns 
invalid_grant (#73234)
2c98ac79d93 is described below

commit 2c98ac79d93b340b664dc8d195d3a633c7b1104b
Author: Shashank Mittal <[email protected]>
AuthorDate: Mon Sep 21 19:00:36 2026 +0530

    Return 403 when Keycloak UMA ticket grant returns invalid_grant (#73234)
    
    Keycloak's uma-ticket grant often returns HTTP 400 invalid_grant when the
    nested access token used as bearer is expired. Treat that the same as 401
    so the API returns 403 instead of raising AirflowException as HTTP 500.
---
 .../keycloak/auth_manager/keycloak_auth_manager.py   |  8 ++++++++
 .../auth_manager/test_keycloak_auth_manager.py       | 20 ++++++++++++++++++++
 2 files changed, 28 insertions(+)

diff --git 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
index d945b4da470..1b8e6d7e630 100644
--- 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
+++ 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/keycloak_auth_manager.py
@@ -523,6 +523,11 @@ class 
KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
             return False
         if resp.status_code == 400:
             error = json.loads(resp.text)
+            # Keycloak's uma-ticket grant often returns 400 invalid_grant (not 
401)
+            # when the nested access token used as bearer is expired or 
invalid.
+            if error.get("error") == "invalid_grant":
+                log.debug("Received invalid_grant from Keycloak: %s", 
resp.text)
+                return False
             if is_team_resource and error.get("error") == "invalid_resource":
                 # filter_authorized_dag_ids will return this error if team 
resources have not been added to the Keycloak Client.
                 log.warning(
@@ -821,6 +826,9 @@ class 
KeycloakAuthManager(BaseAuthManager[KeycloakAuthManagerUser]):
             return set()
         if resp.status_code == 400:
             error = json.loads(resp.text)
+            if error.get("error") == "invalid_grant":
+                log.debug("Received invalid_grant from Keycloak: %s", 
resp.text)
+                return set()
             raise AirflowException(
                 f"Request not recognized by Keycloak. {error.get('error')}. 
{error.get('error_description')}"
             )
diff --git 
a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
 
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
index e52b0c24019..d4d3a192887 100644
--- 
a/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
+++ 
b/providers/keycloak/tests/unit/keycloak/auth_manager/test_keycloak_auth_manager.py
@@ -650,6 +650,16 @@ class TestKeycloakAuthManager:
         assert "Keycloak authorization resource is missing; denying access" in 
caplog.text
         assert "Resource with id [Dag:team-a] does not exist." in caplog.text
 
+    def test_is_authorized_invalid_grant(self, auth_manager, user):
+        resp = Mock()
+        resp.status_code = 400
+        resp.text = '{"error": "invalid_grant", "error_description": "Invalid 
bearer token"}'
+        auth_manager.http_session.post = Mock(return_value=resp)
+
+        result = auth_manager.is_authorized_dag(method="GET", 
details=DagDetails(id="dag_0"), user=user)
+
+        assert result is False
+
     @pytest.mark.parametrize(
         "function",
         [
@@ -1102,6 +1112,16 @@ class TestKeycloakAuthManager:
             token_url, data=payload, headers=headers, timeout=5
         )
 
+    def test_filter_authorized_menu_items_invalid_grant(self, auth_manager, 
user):
+        resp = Mock()
+        resp.status_code = 400
+        resp.text = '{"error": "invalid_grant", "error_description": "Invalid 
bearer token"}'
+        auth_manager.http_session.post = Mock(return_value=resp)
+
+        result = auth_manager.filter_authorized_menu_items([MenuItem.ASSETS, 
MenuItem.CONNECTIONS], user=user)
+
+        assert result == []
+
     def test_get_cli_commands_return_cli_commands(self, auth_manager):
         assert len(auth_manager.get_cli_commands()) == 1
 

Reply via email to