Skip to content

Commit e7d296b

Browse files
Fix issue in OIDC implementation. (#1011)
## What changes are proposed in this pull request? This PR addresses the issue reported in #994 by properly passing the ID Token Source. The PR also slightly refactor the code to make it easier to test. ## How is this tested? Additional unit tests.
1 parent 0ec0dcb commit e7d296b

4 files changed

Lines changed: 77 additions & 12 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
## Release v0.60.0
44

55
### New Features and Improvements
6+
67
* Added headers to HttpRequestResponse in OpenAI client.
78

89
### Bug Fixes
910

11+
- Correctly issue in OIDC implementation that prevented the use of the feature (see #994).
1012
- Fix a reported issue where `FilesExt` fails to retry if it receives certain status code from server.
1113

1214
### Documentation

databricks/sdk/credentials_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def file_oidc(cfg) -> Optional[CredentialsProvider]:
331331
# that provides a Databricks token from an IdTokenSource.
332332
def _oidc_credentials_provider(cfg, id_token_source: oidc.IdTokenSource) -> Optional[CredentialsProvider]:
333333
try:
334-
id_token = id_token_source.id_token()
334+
id_token_source.id_token() # validate the id_token_source
335335
except Exception as e:
336336
logger.debug(f"Failed to get OIDC token: {e}")
337337
return None
@@ -341,7 +341,7 @@ def _oidc_credentials_provider(cfg, id_token_source: oidc.IdTokenSource) -> Opti
341341
token_endpoint=cfg.oidc_endpoints.token_endpoint,
342342
client_id=cfg.client_id,
343343
account_id=cfg.account_id,
344-
id_token=id_token,
344+
id_token_source=id_token_source,
345345
disable_async=cfg.disable_async_token_refresh,
346346
)
347347

databricks/sdk/oidc.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,18 @@ def token(self) -> oauth.Token:
188188
logger.debug("Client ID provided, authenticating with Workload Identity Federation")
189189

190190
id_token = self._id_token_source.id_token()
191+
return self._exchange_id_token(id_token)
191192

193+
# This function is used to create the OAuth client.
194+
# It exists to make it easier to test.
195+
def _exchange_id_token(self, id_token: IdToken) -> oauth.Token:
192196
client = oauth.ClientCredentials(
193197
client_id=self._client_id,
194-
client_secret="", # we have no (rotatable) secrets in OIDC flow
198+
client_secret="",
195199
token_url=self._token_endpoint,
196200
endpoint_params={
197201
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
198-
"subject_token": id_token,
202+
"subject_token": id_token.jwt,
199203
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
200204
},
201205
scopes=["all-apis"],

tests/test_credentials_provider.py

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from datetime import datetime, timedelta
12
from unittest.mock import Mock
23

3-
from databricks.sdk.credentials_provider import external_browser
4+
from databricks.sdk import credentials_provider, oauth, oidc
45

56

7+
# Tests for external_browser function
68
def test_external_browser_refresh_success(mocker):
79
"""Tests successful refresh of existing credentials."""
810

@@ -21,7 +23,9 @@ def test_external_browser_refresh_success(mocker):
2123
mock_token_cache.load.return_value = mock_session_credentials
2224

2325
# Mock SessionCredentials.
24-
want_credentials_provider = lambda c: "new_credentials"
26+
def want_credentials_provider(_):
27+
return "new_credentials"
28+
2529
mock_session_credentials.return_value = want_credentials_provider
2630

2731
# Inject the mock implementations.
@@ -30,7 +34,7 @@ def test_external_browser_refresh_success(mocker):
3034
return_value=mock_token_cache,
3135
)
3236

33-
got_credentials_provider = external_browser(mock_cfg)
37+
got_credentials_provider = credentials_provider.external_browser(mock_cfg)
3438

3539
mock_token_cache.load.assert_called_once()
3640
mock_session_credentials.token.assert_called_once() # Verify token refresh was attempted
@@ -55,7 +59,9 @@ def test_external_browser_refresh_failure_new_oauth_flow(mocker):
5559
mock_token_cache.load.return_value = mock_session_credentials
5660

5761
# Mock SessionCredentials.
58-
want_credentials_provider = lambda c: "new_credentials"
62+
def want_credentials_provider(_):
63+
return "new_credentials"
64+
5965
mock_session_credentials.return_value = want_credentials_provider
6066

6167
# Mock OAuthClient.
@@ -74,7 +80,7 @@ def test_external_browser_refresh_failure_new_oauth_flow(mocker):
7480
return_value=mock_oauth_client,
7581
)
7682

77-
got_credentials_provider = external_browser(mock_cfg)
83+
got_credentials_provider = credentials_provider.external_browser(mock_cfg)
7884

7985
mock_token_cache.load.assert_called_once()
8086
mock_session_credentials.token.assert_called_once() # Refresh attempt
@@ -101,7 +107,10 @@ def test_external_browser_no_cached_credentials(mocker):
101107

102108
# Mock SessionCredentials.
103109
mock_session_credentials = Mock()
104-
want_credentials_provider = lambda c: "new_credentials"
110+
111+
def want_credentials_provider(_):
112+
return "new_credentials"
113+
105114
mock_session_credentials.return_value = want_credentials_provider
106115

107116
# Mock OAuthClient.
@@ -120,7 +129,7 @@ def test_external_browser_no_cached_credentials(mocker):
120129
return_value=mock_oauth_client,
121130
)
122131

123-
got_credentials_provider = external_browser(mock_cfg)
132+
got_credentials_provider = credentials_provider.external_browser(mock_cfg)
124133

125134
mock_token_cache.load.assert_called_once()
126135
mock_oauth_client.initiate_consent.assert_called_once()
@@ -158,8 +167,58 @@ def test_external_browser_consent_fails(mocker):
158167
return_value=mock_oauth_client,
159168
)
160169

161-
got_credentials_provider = external_browser(mock_cfg)
170+
got_credentials_provider = credentials_provider.external_browser(mock_cfg)
162171

163172
mock_token_cache.load.assert_called_once()
164173
mock_oauth_client.initiate_consent.assert_called_once()
165174
assert got_credentials_provider is None
175+
176+
177+
def test_oidc_credentials_provider_invalid_id_token_source():
178+
# Use a mock config object to avoid initializing the auth initialization.
179+
mock_cfg = Mock()
180+
mock_cfg.host = "https://test-workspace.cloud.databricks.com"
181+
mock_cfg.oidc_endpoints = Mock()
182+
mock_cfg.oidc_endpoints.token_endpoint = "https://test-workspace.cloud.databricks.com/oidc/v1/token"
183+
mock_cfg.client_id = "test-client-id"
184+
mock_cfg.account_id = "test-account-id"
185+
mock_cfg.disable_async_token_refresh = True
186+
187+
# An IdTokenSource that raises an error when id_token() is called.
188+
id_token_source = Mock()
189+
id_token_source.id_token.side_effect = ValueError("Invalid ID token source")
190+
191+
cp = credentials_provider._oidc_credentials_provider(mock_cfg, id_token_source)
192+
assert cp is None
193+
194+
195+
def test_oidc_credentials_provider_valid_id_token_source(mocker):
196+
# Use a mock config object to avoid initializing the auth initialization.
197+
mock_cfg = Mock()
198+
mock_cfg.host = "https://test-workspace.cloud.databricks.com"
199+
mock_cfg.oidc_endpoints = Mock()
200+
mock_cfg.oidc_endpoints.token_endpoint = "https://test-workspace.cloud.databricks.com/oidc/v1/token"
201+
mock_cfg.client_id = "test-client-id"
202+
mock_cfg.account_id = "test-account-id"
203+
mock_cfg.disable_async_token_refresh = True
204+
205+
# A valid IdTokenSource that never raises an error.
206+
id_token_source = Mock()
207+
id_token_source.id_token.return_value = oidc.IdToken(jwt="test-jwt-token")
208+
209+
# Mock the _exchange_id_token method on DatabricksOidcTokenSource to return
210+
# a valid oauth.Token based on the IdToken.
211+
def mock_exchange_id_token(id_token: oidc.IdToken):
212+
# Create a token based on the input ID token
213+
return oauth.Token(
214+
access_token=f"exchanged-{id_token.jwt}", token_type="Bearer", expiry=datetime.now() + timedelta(hours=1)
215+
)
216+
217+
mocker.patch.object(oidc.DatabricksOidcTokenSource, "_exchange_id_token", side_effect=mock_exchange_id_token)
218+
219+
cp = credentials_provider._oidc_credentials_provider(mock_cfg, id_token_source)
220+
assert cp is not None
221+
222+
# Test that the credentials provider returns the expected headers
223+
headers = cp()
224+
assert headers == {"Authorization": "Bearer exchanged-test-jwt-token"}

0 commit comments

Comments
 (0)