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 7c75cd9d036 Make refresh_token optional in Keycloak login and token 
services (#75871)
7c75cd9d036 is described below

commit 7c75cd9d0368531102672b31652406dbb8e1e36c
Author: Basant_Jamwal <[email protected]>
AuthorDate: Wed Oct 28 21:16:37 2026 +0530

    Make refresh_token optional in Keycloak login and token services (#75871)
---
 .../keycloak/auth_manager/routes/login.py          | 13 ++++++---
 .../keycloak/auth_manager/services/token.py        |  2 +-
 .../keycloak/auth_manager/routes/test_login.py     | 34 ++++++++++++++++++++++
 .../keycloak/auth_manager/services/test_token.py   | 25 ++++++++++++++++
 4 files changed, 69 insertions(+), 5 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 fdb73e6c966..3040bb16c5d 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
@@ -123,7 +123,7 @@ def login_callback(request: Request):
         user_id=userinfo["sub"],
         name=userinfo["preferred_username"],
         access_token=tokens["access_token"],
-        refresh_token=tokens["refresh_token"],
+        refresh_token=tokens.get("refresh_token"),
     )
     token = get_auth_manager().generate_jwt(user)
     request.state.jwt_token_issued = True
@@ -149,9 +149,14 @@ def login_callback(request: Request):
             COOKIE_NAME_ACCESS_TOKEN, tokens["access_token"], 
path=cookie_path, secure=secure, httponly=True
         )
 
-        response.set_cookie(
-            COOKIE_NAME_REFRESH_TOKEN, tokens["refresh_token"], 
path=cookie_path, secure=secure, httponly=True
-        )
+        if tokens.get("refresh_token"):
+            response.set_cookie(
+                COOKIE_NAME_REFRESH_TOKEN,
+                tokens["refresh_token"],
+                path=cookie_path,
+                secure=secure,
+                httponly=True,
+            )
 
     return response
 
diff --git 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py
 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py
index 6a0566bfce0..2f57e080efa 100644
--- 
a/providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py
+++ 
b/providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py
@@ -67,7 +67,7 @@ def create_token_for(
         user_id=userinfo["sub"],
         name=userinfo["preferred_username"],
         access_token=tokens["access_token"],
-        refresh_token=tokens["refresh_token"],
+        refresh_token=tokens.get("refresh_token"),
     )
 
     return get_auth_manager().generate_api_jwt(user, 
expiration_time_in_seconds=expiration_time_in_seconds)
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 dcfbb3202ae..bfe9b852b8c 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
@@ -84,6 +84,40 @@ class TestLoginRouter:
             assert response.cookies["_access_token"] == "access_token"
             assert response.cookies["_refresh_token"] == "refresh_token"
 
+    
@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_without_refresh_token(
+        self, mock_get_keycloak_client, mock_get_auth_manager, client
+    ):
+        code = "code"
+        token = "token"
+        state = "state"
+        mock_keycloak_client = Mock()
+        mock_keycloak_client.token.return_value = {
+            "access_token": "access_token",
+            "id_token": "id_token",
+            # no refresh_token key -- simulates Keycloak with "Use Refresh 
Tokens" disabled
+        }
+        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
+        response = client.get(
+            AUTH_MANAGER_FASTAPI_APP_PREFIX + 
f"/login_callback?code={code}&state={state}",
+            follow_redirects=False,
+            cookies={"_oauth_state": state},
+        )
+        mock_auth_manager.generate_jwt.assert_called_once()
+        user = mock_auth_manager.generate_jwt.call_args[0][0]
+        assert user.get_id() == "sub"
+        assert user.refresh_token is None
+        assert response.status_code == 303
+        assert "_refresh_token" not in response.cookies
+
     
@patch("airflow.providers.keycloak.auth_manager.routes.login.KeycloakAuthManager.get_keycloak_client")
     def test_login_sets_secure_state_cookie_behind_tls_proxy(self, 
mock_get_keycloak_client, client):
         mock_keycloak_client = Mock()
diff --git 
a/providers/keycloak/tests/unit/keycloak/auth_manager/services/test_token.py 
b/providers/keycloak/tests/unit/keycloak/auth_manager/services/test_token.py
index 1a5b8aa5a8a..ada21b9b8db 100644
--- a/providers/keycloak/tests/unit/keycloak/auth_manager/services/test_token.py
+++ b/providers/keycloak/tests/unit/keycloak/auth_manager/services/test_token.py
@@ -68,6 +68,31 @@ class TestTokenService:
         mock_keycloak_client.token.assert_called_once_with(self.test_username, 
self.test_password)
         
mock_keycloak_client.userinfo.assert_called_once_with(self.test_access_token)
 
+    @conf_vars(
+        {
+            ("api_auth", "jwt_expiration_time"): "10",
+        }
+    )
+    
@patch("airflow.providers.keycloak.auth_manager.services.token.get_auth_manager")
+    
@patch("airflow.providers.keycloak.auth_manager.services.token.KeycloakAuthManager.get_keycloak_client")
+    def test_create_token_without_refresh_token(self, 
mock_get_keycloak_client, mock_get_auth_manager):
+        mock_keycloak_client = Mock()
+        mock_keycloak_client.token.return_value = {
+            "access_token": self.test_access_token,
+            # no refresh_token key, simulates Keycloak with "Use Refresh 
Tokens" disabled
+        }
+        mock_keycloak_client.userinfo.return_value = {"sub": "sub", 
"preferred_username": "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_api_jwt.return_value = self.token
+
+        assert create_token_for(username=self.test_username, 
password=self.test_password) == self.token
+        mock_auth_manager.generate_api_jwt.assert_called_once()
+        user = mock_auth_manager.generate_api_jwt.call_args[0][0]
+        assert user.access_token == self.test_access_token
+        assert user.refresh_token is None
+
     @conf_vars(
         {
             ("api_auth", "jwt_cli_expiration_time"): "10",

Reply via email to