Skip to content

Commit a211af3

Browse files
committed
added Azure DevOps OIDC authentication
1 parent d2f29fe commit a211af3

File tree

2 files changed

+97
-0
lines changed

2 files changed

+97
-0
lines changed

databricks/sdk/credentials_provider.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,57 @@ def token() -> oauth.Token:
406406

407407
return OAuthCredentialsProvider(refreshed_headers, token)
408408

409+
@oauth_credentials_strategy("azdo-oidc", ["host", "client_id"])
410+
def azure_devops_oidc(cfg: "Config") -> Optional[CredentialsProvider]:
411+
"""
412+
Azure DevOps OIDC authentication uses a Token Supplier to get a JWT Token
413+
and exchanges it for a Databricks Token.
414+
415+
Supported in Azure DevOps pipelines with OIDC service connections.
416+
"""
417+
supplier = oidc_token_supplier.AzureDevOpsOIDCTokenSupplier()
418+
419+
audience = cfg.token_audience
420+
if audience is None and cfg.is_account_client:
421+
audience = cfg.account_id
422+
if audience is None and not cfg.is_account_client:
423+
audience = cfg.oidc_endpoints.token_endpoint
424+
425+
# Try to get an idToken. If no supplier returns a token, we cannot use this authentication mode.
426+
id_token = supplier.get_oidc_token(audience)
427+
if not id_token:
428+
return None
429+
430+
def token_source_for(audience: str) -> oauth.TokenSource:
431+
id_token = supplier.get_oidc_token(audience)
432+
if not id_token:
433+
# Should not happen, since we checked it above.
434+
raise Exception("Cannot get Azure DevOps OIDC token")
435+
436+
return oauth.ClientCredentials(
437+
client_id=cfg.client_id,
438+
client_secret="", # we have no (rotatable) secrets in OIDC flow
439+
token_url=cfg.oidc_endpoints.token_endpoint,
440+
endpoint_params={
441+
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
442+
"subject_token": id_token,
443+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
444+
},
445+
scopes=["all-apis"],
446+
use_params=True,
447+
disable_async=cfg.disable_async_token_refresh,
448+
)
449+
450+
def refreshed_headers() -> Dict[str, str]:
451+
token = token_source_for(audience).token()
452+
return {"Authorization": f"{token.token_type} {token.access_token}"}
453+
454+
def token() -> oauth.Token:
455+
return token_source_for(audience).token()
456+
457+
return OAuthCredentialsProvider(refreshed_headers, token)
458+
459+
409460

410461
@oauth_credentials_strategy("github-oidc-azure", ["host", "azure_client_id"])
411462
def github_oidc_azure(cfg: "Config") -> Optional[CredentialsProvider]:
@@ -1015,6 +1066,7 @@ def __init__(self) -> None:
10151066
env_oidc,
10161067
file_oidc,
10171068
github_oidc,
1069+
azure_devops_oidc,
10181070
azure_service_principal,
10191071
github_oidc_azure,
10201072
azure_cli,

databricks/sdk/oidc_token_supplier.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,48 @@ def get_oidc_token(self, audience: str) -> Optional[str]:
2626
return None
2727

2828
return response_json["value"]
29+
30+
31+
class AzureDevOpsOIDCTokenSupplier:
32+
"""
33+
Supplies OIDC tokens from Azure DevOps pipelines.
34+
"""
35+
36+
def get_oidc_token(self, audience: str) -> Optional[str]:
37+
# Retrieve necessary environment variables
38+
access_token = os.environ.get('SYSTEM_ACCESSTOKEN')
39+
collection_uri = os.environ.get('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI')
40+
project_id = os.environ.get('SYSTEM_TEAMPROJECT')
41+
plan_id = os.environ.get('SYSTEM_PLANID')
42+
job_id = os.environ.get('SYSTEM_JOBID')
43+
hub_name = 'build' # Or 'release' for release pipelines
44+
45+
# Check for required variables
46+
if not all([access_token, collection_uri, project_id, plan_id, job_id]):
47+
# not in Azure DevOps pipeline
48+
return None
49+
50+
# Construct the URL
51+
request_url = f"{collection_uri}{project_id}/_apis/distributedtask/hubs/{hub_name}/plans/{plan_id}/jobs/{job_id}/oidctoken?api-version=7.1-preview.1"
52+
53+
# See https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables
54+
headers = {
55+
"Authorization": f"Bearer {access_token}",
56+
"Content-Type": "application/json"
57+
}
58+
59+
# Set up the JSON payload for the request body
60+
payload = {
61+
'audience': audience
62+
}
63+
try:
64+
# Make the POST request
65+
response = requests.post(request_url, headers=headers, json=payload)
66+
response.raise_for_status() # Raise an exception for bad status codes
67+
68+
# Return the OIDC token from the JSON response
69+
return response.json()['oidcToken']
70+
except requests.exceptions.RequestException as e:
71+
print(f"Error requesting OIDC token: {e}")
72+
return None
73+

0 commit comments

Comments
 (0)