-
Notifications
You must be signed in to change notification settings - Fork 35
Add support for authentication through Azure MSI #760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Divyansh-db
merged 4 commits into
main
from
divyansh-vijayvergia_data/stack/azure-msi-support
Apr 17, 2026
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8d4f016
add support for authentication through Azure MSI
Divyansh-db 222a66e
Merge branch 'main' into divyansh-vijayvergia_data/stack/azure-msi-su…
Divyansh-db 22d25df
addressed comments
Divyansh-db d13f052
Merge branch 'main' into divyansh-vijayvergia_data/stack/azure-msi-su…
Divyansh-db File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureMsiCredentialsProvider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package com.databricks.sdk.core; | ||
|
|
||
| import com.databricks.sdk.core.oauth.AzureMsiTokenSource; | ||
| import com.databricks.sdk.core.oauth.CachedTokenSource; | ||
| import com.databricks.sdk.core.oauth.OAuthHeaderFactory; | ||
| import com.databricks.sdk.core.oauth.Token; | ||
| import com.databricks.sdk.core.utils.AzureUtils; | ||
| import com.databricks.sdk.support.InternalApi; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Adds refreshed Azure Active Directory (AAD) tokens obtained via Azure Managed Service Identity | ||
| * (MSI) to every request. This provider authenticates using the Azure Instance Metadata Service | ||
| * (IMDS) endpoint, which is available on Azure VMs and other compute resources with managed | ||
| * identities enabled. | ||
| */ | ||
| @InternalApi | ||
| public class AzureMsiCredentialsProvider implements CredentialsProvider { | ||
| private static final Logger LOG = LoggerFactory.getLogger(AzureMsiCredentialsProvider.class); | ||
| private final ObjectMapper mapper = new ObjectMapper(); | ||
|
|
||
| @Override | ||
| public String authType() { | ||
| return "azure-msi"; | ||
| } | ||
|
|
||
| @Override | ||
| public OAuthHeaderFactory configure(DatabricksConfig config) { | ||
| if (!config.isAzure()) { | ||
| return null; | ||
| } | ||
|
|
||
| if (!isAzureUseMsi(config)) { | ||
| return null; | ||
| } | ||
|
|
||
| if (config.getAzureWorkspaceResourceId() == null && config.getHost() == null) { | ||
| return null; | ||
| } | ||
|
|
||
| LOG.debug("Generating AAD token via Azure MSI"); | ||
|
|
||
| AzureUtils.ensureHostPresent(config, mapper, this::tokenSourceFor); | ||
|
|
||
| CachedTokenSource inner = tokenSourceFor(config, config.getEffectiveAzureLoginAppId()); | ||
| CachedTokenSource cloud = | ||
| tokenSourceFor(config, config.getAzureEnvironment().getServiceManagementEndpoint()); | ||
|
|
||
| return OAuthHeaderFactory.fromSuppliers( | ||
| inner::getToken, | ||
| () -> { | ||
| Token token = inner.getToken(); | ||
| Map<String, String> headers = new HashMap<>(); | ||
| headers.put("Authorization", "Bearer " + token.getAccessToken()); | ||
| AzureUtils.addSpManagementToken(cloud, headers); | ||
| AzureUtils.addWorkspaceResourceId(config, headers); | ||
| return headers; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Null-safe check for the azureUseMsi config flag. The underlying field is a boxed Boolean, but | ||
| * the getter auto-unboxes to primitive boolean, which would NPE when the field is null. This | ||
| * helper treats null as false. | ||
| */ | ||
| private static boolean isAzureUseMsi(DatabricksConfig config) { | ||
| try { | ||
| return config.getAzureUseMsi(); | ||
| } catch (NullPointerException e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a CachedTokenSource for the specified Azure resource using MSI authentication. | ||
| * | ||
| * @param config The DatabricksConfig instance containing the required authentication parameters. | ||
| * @param resource The Azure resource for which OAuth tokens need to be fetched. | ||
| * @return A CachedTokenSource instance capable of fetching OAuth tokens for the specified Azure | ||
| * resource. | ||
| */ | ||
| CachedTokenSource tokenSourceFor(DatabricksConfig config, String resource) { | ||
| AzureMsiTokenSource tokenSource = | ||
| new AzureMsiTokenSource(config.getHttpClient(), resource, config.getAzureClientId()); | ||
| return new CachedTokenSource.Builder(tokenSource) | ||
| .setAsyncDisabled(config.getDisableAsyncTokenRefresh()) | ||
| .build(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureMsiTokenSource.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package com.databricks.sdk.core.oauth; | ||
|
|
||
| import com.databricks.sdk.core.DatabricksException; | ||
| import com.databricks.sdk.core.http.HttpClient; | ||
| import com.databricks.sdk.core.http.Request; | ||
| import com.databricks.sdk.core.http.Response; | ||
| import com.databricks.sdk.support.InternalApi; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import java.io.IOException; | ||
| import java.time.Instant; | ||
|
|
||
| /** | ||
| * A {@link TokenSource} that fetches OAuth tokens from the Azure Instance Metadata Service (IMDS) | ||
| * endpoint for Managed Service Identity (MSI) authentication. | ||
| * | ||
| * <p>This token source makes HTTP GET requests to the well-known IMDS endpoint at {@code | ||
| * http://169.254.169.254/metadata/identity/oauth2/token} to obtain access tokens for the specified | ||
| * Azure resource. | ||
| */ | ||
| @InternalApi | ||
| public class AzureMsiTokenSource implements TokenSource { | ||
|
|
||
| private static final String IMDS_ENDPOINT = | ||
| "http://169.254.169.254/metadata/identity/oauth2/token"; | ||
|
|
||
| private final HttpClient httpClient; | ||
| private final String resource; | ||
| private final String clientId; | ||
| private final ObjectMapper mapper = new ObjectMapper(); | ||
|
|
||
| /** Response structure from the Azure IMDS token endpoint. */ | ||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||
| static class MsiTokenResponse { | ||
| @JsonProperty("token_type") | ||
| private String tokenType; | ||
|
|
||
| @JsonProperty("access_token") | ||
| private String accessToken; | ||
|
|
||
| @JsonProperty("expires_on") | ||
| private String expiresOn; | ||
|
|
||
| Token toToken() { | ||
| if (accessToken == null || accessToken.isEmpty()) { | ||
| throw new DatabricksException("MSI token response missing or empty 'access_token' field"); | ||
| } | ||
| if (tokenType == null || tokenType.isEmpty()) { | ||
| throw new DatabricksException("MSI token response missing or empty 'token_type' field"); | ||
| } | ||
| if (expiresOn == null || expiresOn.isEmpty()) { | ||
| throw new DatabricksException("MSI token response missing 'expires_on' field"); | ||
| } | ||
| long epoch; | ||
| try { | ||
| epoch = Long.parseLong(expiresOn); | ||
| } catch (NumberFormatException e) { | ||
| throw new DatabricksException( | ||
| "Invalid 'expires_on' value in MSI token response: " + expiresOn, e); | ||
| } | ||
| return new Token(accessToken, tokenType, Instant.ofEpochSecond(epoch)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new AzureMsiTokenSource. | ||
| * | ||
| * @param httpClient The HTTP client to use for requests to the IMDS endpoint. | ||
| * @param resource The Azure resource for which to obtain an access token. | ||
| * @param clientId The client ID of the managed identity to use. May be null for system-assigned | ||
| * identities. | ||
| */ | ||
| public AzureMsiTokenSource(HttpClient httpClient, String resource, String clientId) { | ||
| this.httpClient = httpClient; | ||
| this.resource = resource; | ||
| this.clientId = clientId; | ||
| } | ||
|
|
||
| @Override | ||
| public Token getToken() { | ||
| Request req = new Request("GET", IMDS_ENDPOINT); | ||
| req.withQueryParam("api-version", "2018-02-01"); | ||
| req.withQueryParam("resource", resource); | ||
| if (clientId != null && !clientId.isEmpty()) { | ||
| req.withQueryParam("client_id", clientId); | ||
| } | ||
| req.withHeader("Metadata", "true"); | ||
|
|
||
| Response resp; | ||
| try { | ||
| resp = httpClient.execute(req); | ||
| } catch (IOException e) { | ||
| throw new DatabricksException( | ||
| "Failed to request MSI token from IMDS endpoint: " + e.getMessage(), e); | ||
| } | ||
|
|
||
| if (resp.getStatusCode() != 200) { | ||
| throw new DatabricksException( | ||
| "Failed to request MSI token: status code " | ||
| + resp.getStatusCode() | ||
| + ", response body: " | ||
| + resp.getDebugBody()); | ||
| } | ||
|
|
||
| try { | ||
| MsiTokenResponse msiToken = mapper.readValue(resp.getBody(), MsiTokenResponse.class); | ||
| return msiToken.toToken(); | ||
| } catch (IOException e) { | ||
| throw new DatabricksException("Failed to parse MSI token response: " + e.getMessage(), e); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Causing a null-pointer on purpose and using a catch is very unorthodox. Better use: