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",