Skip to content

Commit fcddd16

Browse files
authored
[DECO-25297] Add native support for Azure DevOps OIDC authentication (#1264)
Added native support for Azure DevOps OIDC authentication to allow authentication from Azure DevOps pipeline's environment. ## Testing Tested on an Azure DevOps project. Used the SDK to authenticate with a Databrick's workspace using Azure DevOps OIDC 1) Created a demo python files that uses the SDK to create a Workspace Client: This will test if the OIDC authentication is working <img width="1904" height="941" alt="Screenshot 2025-09-18 at 18 07 10" src="https://github.com/user-attachments/assets/3c143c42-666c-4042-8b58-4c328bc1cccd" /> 2) Created a pipeline to run the demo file. Set the necessary environment variables. (System.AccessToken is slightly different, it needs to be exported through pipeline syntax: https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken) <img width="1913" height="984" alt="Screenshot 2025-09-18 at 18 07 26" src="https://github.com/user-attachments/assets/a82fed9c-9f3b-4527-9dea-97995bd9f696" /> 3) The pipeline runs successfully indicating that authentication succeeded. <img width="1909" height="982" alt="Screenshot 2025-09-18 at 18 08 38" src="https://github.com/user-attachments/assets/bcd527f5-33d0-47be-9748-6951abe2bdb0" />
1 parent f307a2a commit fcddd16

5 files changed

Lines changed: 121 additions & 1 deletion

File tree

NEXT_CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### New Features and Improvements
66

7+
* Added native support for authentication through Azure DevOps OIDC
8+
79
### Bug Fixes
810

911
### Documentation

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,11 @@ Depending on the Databricks authentication method, the SDK uses the following in
174174

175175
### Databricks native authentication
176176

177-
By default, the Databricks SDK for Go initially tries Databricks token authentication (`AuthType: "pat"` in `*databricks.Config`). If the SDK is unsuccessful, it then tries Workload Identity Federation (WIF) based authentication(`AuthType: "github-oidc"` in `*databricks.Config`). Currently, only GitHub provided JWT Tokens is supported.
177+
By default, the Databricks SDK for Go initially tries Databricks token authentication (`AuthType: "pat"` in `*databricks.Config`). If the SDK is unsuccessful, it then tries Workload Identity Federation (WIF). See [Supported WIF](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-federation-provider) for the supported JWT token providers.
178178

179179
- For Databricks token authentication, you must provide `Host` and `Token`; or their environment variable or `.databrickscfg` file field equivalents.
180180
- For Databricks OIDC authentication, you must provide the `Host`, `ClientId` and `TokenAudience` _(optional)_ either directly, through the corresponding environment variables, or in your `.databrickscfg` configuration file. More information can be found in [Databricks Documentation](https://docs.databricks.com/aws/en/dev-tools/auth/oauth-federation#workload-identity-federation)
181+
- For Azure DevOps OIDC authentication, the `TokenAudience` is irrelevant as the audience is always set to `api://AzureADTokenExchange`. Also, the `System.AccessToken` pipeline variable required for OIDC request must be exposed as the `SYSTEM_ACCESSTOKEN` environment variable, following [Pipeline variables](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken)
181182

182183
| `*databricks.Config` argument | Description | Environment variable / `.databrickscfg` file field |
183184
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- |

config/auth_default.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func (c *DefaultCredentials) Configure(ctx context.Context, cfg *Config) (creden
4848
MetadataServiceCredentials{},
4949
// OIDC Strategies.
5050
githubOIDC(cfg),
51+
azureDevOpsOIDC(cfg),
5152
envOIDC(cfg),
5253
fileOIDC(cfg),
5354
// Azure strategies.
@@ -98,6 +99,18 @@ func githubOIDC(cfg *Config) CredentialsStrategy {
9899
))
99100
}
100101

102+
func azureDevOpsOIDC(cfg *Config) CredentialsStrategy {
103+
return oidcStrategy(cfg, "azure-devops-oidc", oidc.NewAzureDevOpsIDTokenSource(
104+
cfg.refreshClient,
105+
cfg.AzureDevOpsAccessToken,
106+
cfg.AzureDevOpsTeamFoundationCollectionUri,
107+
cfg.AzureDevOpsPlanId,
108+
cfg.AzureDevOpsJobId,
109+
cfg.AzureDevOpsTeamProjectId,
110+
cfg.AzureDevOpsHostType,
111+
))
112+
}
113+
101114
func envOIDC(cfg *Config) CredentialsStrategy {
102115
v := cfg.OIDCTokenEnv
103116
if v == "" {

config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ type Config struct {
9191
ActionsIDTokenRequestURL string `name:"actions_id_token_request_url" env:"ACTIONS_ID_TOKEN_REQUEST_URL"`
9292
ActionsIDTokenRequestToken string `name:"actions_id_token_request_token" env:"ACTIONS_ID_TOKEN_REQUEST_TOKEN"`
9393

94+
// Parameters to request Azure DevOps OIDC token on behalf of Azure DevOps Pipelines.
95+
// Ref: https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables
96+
AzureDevOpsAccessToken string `name:"azure_devops_access_token" env:"SYSTEM_ACCESSTOKEN"`
97+
AzureDevOpsTeamFoundationCollectionUri string `name:"azure_devops_team_foundation_collection_uri" env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"`
98+
AzureDevOpsPlanId string `name:"azure_devops_plan_id" env:"SYSTEM_PLANID"`
99+
AzureDevOpsJobId string `name:"azure_devops_job_id" env:"SYSTEM_JOBID"`
100+
AzureDevOpsTeamProjectId string `name:"azure_devops_team_project_id" env:"SYSTEM_TEAMPROJECTID"`
101+
AzureDevOpsHostType string `name:"azure_devops_host_type" env:"SYSTEM_HOSTTYPE"`
102+
94103
// AzureEnvironment (PUBLIC, USGOVERNMENT, CHINA) has specific set of API endpoints. Starting from v0.26.0,
95104
// the environment is determined based on the workspace hostname, if it's specified.
96105
AzureEnvironment string `name:"azure_environment" env:"ARM_ENVIRONMENT"`
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package oidc
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/databricks/databricks-sdk-go/httpclient"
9+
"github.com/databricks/databricks-sdk-go/logger"
10+
)
11+
12+
// NewAzureDevOpsIDTokenSource returns a new IDTokenSource that retrieves an IDToken
13+
// from the Azure DevOps environment. This IDTokenSource is only valid when
14+
// running in Azure DevOps Pipelines with OIDC enabled.
15+
func NewAzureDevOpsIDTokenSource(client *httpclient.ApiClient, azureDevOpsAccessToken, azureDevOpsTeamFoundationCollectionUri, azureDevOpsPlanId, azureDevOpsJobId, azureDevOpsTeamProjectId, azureDevOpsHostType string) IDTokenSource {
16+
return &azureDevOpsIDTokenSource{
17+
azureDevOpsAccessToken: azureDevOpsAccessToken,
18+
azureDevOpsTeamFoundationCollectionUri: azureDevOpsTeamFoundationCollectionUri,
19+
refreshClient: client,
20+
azureDevOpsPlanId: azureDevOpsPlanId,
21+
azureDevOpsJobId: azureDevOpsJobId,
22+
azureDevOpsTeamProjectId: azureDevOpsTeamProjectId,
23+
azureDevOpsHostType: azureDevOpsHostType,
24+
}
25+
}
26+
27+
// azureDevOpsIDTokenSource retrieves JWT Tokens from Azure DevOps Pipelines.
28+
type azureDevOpsIDTokenSource struct {
29+
azureDevOpsAccessToken string
30+
azureDevOpsTeamFoundationCollectionUri string
31+
refreshClient *httpclient.ApiClient
32+
azureDevOpsPlanId string
33+
azureDevOpsJobId string
34+
azureDevOpsTeamProjectId string
35+
azureDevOpsHostType string
36+
}
37+
38+
// IDToken returns a JWT Token for the specified audience. For Azure DevOps OIDC,
39+
// the audience parameter is ignored as Azure DevOps tokens always use "api://AzureADTokenExchange".
40+
// It will return an error if not running in Azure DevOps Pipelines.
41+
func (a *azureDevOpsIDTokenSource) IDToken(ctx context.Context, audience string) (*IDToken, error) {
42+
if a.azureDevOpsAccessToken == "" {
43+
logger.Debugf(ctx, "Missing AZUREDEVOPS_ACCESSTOKEN, likely not calling from Azure DevOps Pipeline")
44+
return nil, errors.New("missing AZUREDEVOPS_ACCESSTOKEN")
45+
}
46+
if a.azureDevOpsTeamFoundationCollectionUri == "" {
47+
logger.Debugf(ctx, "Missing AZUREDEVOPS_TEAMFOUNDATIONCOLLECTIONURI, likely not calling from Azure DevOps Pipeline")
48+
return nil, errors.New("missing AZUREDEVOPS_TEAMFOUNDATIONCOLLECTIONURI")
49+
}
50+
if a.azureDevOpsPlanId == "" {
51+
logger.Debugf(ctx, "Missing AZUREDEVOPS_PLANID, likely not calling from Azure DevOps Pipeline")
52+
return nil, errors.New("missing AZUREDEVOPS_PLANID")
53+
}
54+
if a.azureDevOpsJobId == "" {
55+
logger.Debugf(ctx, "Missing AZUREDEVOPS_JOBID, likely not calling from Azure DevOps Pipeline")
56+
return nil, errors.New("missing AZUREDEVOPS_JOBID")
57+
}
58+
if a.azureDevOpsTeamProjectId == "" {
59+
logger.Debugf(ctx, "Missing AZUREDEVOPS_TEAMPROJECTID, likely not calling from Azure DevOps Pipeline")
60+
return nil, errors.New("missing AZUREDEVOPS_TEAMPROJECTID")
61+
}
62+
if a.azureDevOpsHostType == "" {
63+
logger.Debugf(ctx, "Missing AZUREDEVOPS_HOSTTYPE, likely not calling from Azure DevOps Pipeline")
64+
return nil, errors.New("missing AZUREDEVOPS_HOSTTYPE")
65+
}
66+
67+
// Azure DevOps OIDC endpoint format
68+
// Reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create?view=azure-devops-rest-7.1
69+
// Use azureDevOpsHostType to determine the hub name dynamically (e.g., "build", "release", etc.)
70+
requestUrl := fmt.Sprintf("%s/%s/_apis/distributedtask/hubs/%s/plans/%s/jobs/%s/oidctoken?api-version=7.2-preview.1",
71+
a.azureDevOpsTeamFoundationCollectionUri,
72+
a.azureDevOpsTeamProjectId,
73+
a.azureDevOpsHostType,
74+
a.azureDevOpsPlanId,
75+
a.azureDevOpsJobId)
76+
77+
// Azure DevOps returns {"oidcToken":"***"} format, not {"value":"***"}
78+
var azureResp struct {
79+
OidcToken string `json:"oidcToken"`
80+
}
81+
82+
err := a.refreshClient.Do(ctx, "POST", requestUrl,
83+
httpclient.WithRequestHeader("Authorization", fmt.Sprintf("Bearer %s", a.azureDevOpsAccessToken)),
84+
httpclient.WithResponseUnmarshal(&azureResp),
85+
)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to request ID token from Azure DevOps: %w", err)
88+
}
89+
90+
if azureResp.OidcToken == "" {
91+
return nil, fmt.Errorf("empty OIDC token received from Azure DevOps")
92+
}
93+
94+
return &IDToken{Value: azureResp.OidcToken}, nil
95+
}

0 commit comments

Comments
 (0)