Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,69 @@

import com.databricks.sdk.core.oauth.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The DefaultCredentialsProvider is the primary authentication handler for the Databricks SDK. It
* implements a chain of responsibility pattern to manage multiple authentication methods, including
* Personal Access Tokens (PAT), OAuth, Azure, Google, and OpenID Connect (OIDC). The provider
* attempts each authentication method in sequence until a valid credential is obtained.
*/
public class DefaultCredentialsProvider implements CredentialsProvider {
private static final Logger LOG = LoggerFactory.getLogger(DefaultCredentialsProvider.class);

private static final List<Class<?>> providerClasses =
Arrays.asList(
PatCredentialsProvider.class,
BasicCredentialsProvider.class,
OAuthM2MServicePrincipalCredentialsProvider.class,
GithubOidcCredentialsProvider.class,
AzureGithubOidcCredentialsProvider.class,
AzureServicePrincipalCredentialsProvider.class,
AzureCliCredentialsProvider.class,
ExternalBrowserCredentialsProvider.class,
DatabricksCliCredentialsProvider.class,
NotebookNativeCredentialsProvider.class,
GoogleCredentialsCredentialsProvider.class,
GoogleIdCredentialsProvider.class);

private final List<CredentialsProvider> providers;
/* List of credential providers that will be tried in sequence */
private List<CredentialsProvider> providers = new ArrayList<>();

/* The currently selected authentication type */
private String authType = "default";

public String authType() {
return authType;
}
/**
* Internal class to associate an ID token source with a name for identification purposes. Used
* primarily for OIDC (OpenID Connect) authentication flows.
*/
private static class NamedIDTokenSource {
private final String name;
private final IDTokenSource idTokenSource;

public DefaultCredentialsProvider() {
providers = new ArrayList<>();
for (Class<?> clazz : providerClasses) {
try {
providers.add((CredentialsProvider) clazz.newInstance());
} catch (NoClassDefFoundError | InstantiationException | IllegalAccessException e) {
LOG.warn(
"Failed to instantiate credentials provider: "
+ clazz.getName()
+ ", skipping. Cause: "
+ e.getClass().getCanonicalName()
+ ": "
+ e.getMessage());
}
public NamedIDTokenSource(String name, IDTokenSource idTokenSource) {
this.name = name;
this.idTokenSource = idTokenSource;
}

public String getName() {
return name;
}

public IDTokenSource getIdTokenSource() {
return idTokenSource;
}
}

public DefaultCredentialsProvider() {}

/**
* Returns the current authentication type being used
*
* @return String representing the authentication type
*/
public String authType() {
return authType;
}

/**
* Configures the credentials provider with the given Databricks configuration. This method tries
* each available credential provider in sequence until one succeeds.
*
* @param config The Databricks configuration containing authentication details
* @return HeaderFactory for making authenticated requests
* @throws DatabricksException if no valid credentials can be configured
*/
@Override
public synchronized HeaderFactory configure(DatabricksConfig config) {
addDefaultCredentialsProviders(config);
for (CredentialsProvider provider : providers) {
if (config.getAuthType() != null
&& !config.getAuthType().isEmpty()
Expand Down Expand Up @@ -80,4 +93,77 @@ public synchronized HeaderFactory configure(DatabricksConfig config) {
+ authFlowUrl
+ " to configure credentials for your preferred authentication method");
}

/**
* Adds OpenID Connect (OIDC) based credential providers to the list of available providers.
*
* @param config The Databricks configuration containing OIDC settings
*/
private void addOIDCCredentialsProviders(DatabricksConfig config) {
// TODO: refactor the code so that the IdTokenSources are created within the
// configure call of their corresponding CredentialsProvider. This will allow
// us to simplify the code by validating IdTokenSources when they are created.
OpenIDConnectEndpoints endpoints = null;
try {
endpoints = config.getOidcEndpoints();
} catch (Exception e) {
LOG.warn("Failed to get OpenID Connect endpoints", e);
}

List<NamedIDTokenSource> namedIdTokenSources = new ArrayList<>();
namedIdTokenSources.add(
new NamedIDTokenSource(
"github-oidc",
new GithubIDTokenSource(
config.getActionsIdTokenRequestUrl(),
config.getActionsIdTokenRequestToken(),
config.getHttpClient())));
// Add new IDTokenSources and ID providers here. Example:
// namedIdTokenSources.add(new NamedIDTokenSource("custom-oidc", new CustomIDTokenSource(...)));

// Configure OAuth token sources for each ID token source
for (NamedIDTokenSource namedIdTokenSource : namedIdTokenSources) {
DatabricksOAuthTokenSource oauthTokenSource =
new DatabricksOAuthTokenSource.Builder(
config.getClientId(),
config.getHost(),
endpoints,
namedIdTokenSource.getIdTokenSource(),
config.getHttpClient())
.audience(config.getTokenAudience())
.accountId(config.isAccountClient() ? config.getAccountId() : null)
.build();

providers.add(
new TokenSourceCredentialsProvider(oauthTokenSource, namedIdTokenSource.getName()));
}
}

/**
* Initializes all available credential providers in the preferred order. The order of providers
* determines the authentication fallback sequence.
*
* @param config The Databricks configuration to use for provider initialization
*/
private synchronized void addDefaultCredentialsProviders(DatabricksConfig config) {
if (!providers.isEmpty()) {
return;
}

providers.add(new PatCredentialsProvider());
providers.add(new BasicCredentialsProvider());
providers.add(new OAuthM2MServicePrincipalCredentialsProvider());

// Add OIDC-based providers
addOIDCCredentialsProviders(config);

providers.add(new AzureGithubOidcCredentialsProvider());
providers.add(new AzureServicePrincipalCredentialsProvider());
providers.add(new AzureCliCredentialsProvider());
providers.add(new ExternalBrowserCredentialsProvider());
providers.add(new DatabricksCliCredentialsProvider());
providers.add(new NotebookNativeCredentialsProvider());
providers.add(new GoogleCredentialsCredentialsProvider());
providers.add(new GoogleIdCredentialsProvider());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import java.io.IOException;

/**
* GithubIDTokenSource retrieves JWT Tokens from GitHub Actions. This class implements the
* IDTokenSource interface and provides a method for obtaining ID tokens specifically from GitHub
* Actions environment.
*/
public class GithubIDTokenSource implements IDTokenSource {
/* URL endpoint for requesting ID tokens from GitHub Actions */
private final String actionsIDTokenRequestURL;
/* Authentication token required to request ID tokens from GitHub Actions */
private final String actionsIDTokenRequestToken;
/* HTTP client for making requests to GitHub Actions */
private final HttpClient httpClient;
/* JSON mapper for parsing response data */
private static final ObjectMapper mapper = new ObjectMapper();

/**
* Constructs a new GithubIDTokenSource.
*
* @param actionsIDTokenRequestURL The URL to request the ID token from GitHub Actions.
* @param actionsIDTokenRequestToken The token used to authenticate the request.
* @param httpClient The HTTP client to use for making requests.
*/
public GithubIDTokenSource(
String actionsIDTokenRequestURL, String actionsIDTokenRequestToken, HttpClient httpClient) {
this.actionsIDTokenRequestURL = actionsIDTokenRequestURL;
this.actionsIDTokenRequestToken = actionsIDTokenRequestToken;
this.httpClient = httpClient;
}

/**
* Retrieves an ID token from GitHub Actions. This method makes an authenticated request to GitHub
* Actions to obtain a JWT token that later can be exchanged for a Databricks access token.
*
* @param audience Optional audience claim for the token. If provided, it will be included in the
* token request to GitHub Actions.
* @return An IDToken object containing the JWT token value
* @throws DatabricksException if the token request fails or if required configuration is missing
*/
@Override
public IDToken getIDToken(String audience) {
// Validate required configuration
if (Strings.isNullOrEmpty(actionsIDTokenRequestURL)) {
throw new DatabricksException("Missing ActionsIDTokenRequestURL");
}
if (Strings.isNullOrEmpty(actionsIDTokenRequestToken)) {
throw new DatabricksException("Missing ActionsIDTokenRequestToken");
}
if (httpClient == null) {
throw new DatabricksException("HttpClient cannot be null");
}

String requestUrl = actionsIDTokenRequestURL;
if (!Strings.isNullOrEmpty(audience)) {
requestUrl = String.format("%s&audience=%s", requestUrl, audience);
}

Request req =
new Request("GET", requestUrl)
.withHeader("Authorization", "Bearer " + actionsIDTokenRequestToken);

Response resp;
try {
resp = httpClient.execute(req);
} catch (IOException e) {
throw new DatabricksException(
"Failed to request ID token from " + requestUrl + ": " + e.getMessage(), e);
}

// Validate response status code
if (resp.getStatusCode() != 200) {
throw new DatabricksException(
"Failed to request ID token: status code "
+ resp.getStatusCode()
+ ", response body: "
+ resp.getBody().toString());
}

// Parse the JSON response
ObjectNode jsonResp;
try {
jsonResp = mapper.readValue(resp.getBody(), ObjectNode.class);
} catch (IOException e) {
throw new DatabricksException(
"Failed to request ID token: corrupted token: " + e.getMessage());
}

// Validate response structure and token value
if (!jsonResp.has("value")) {
throw new DatabricksException("ID token response missing 'value' field");
}

try {
String tokenValue = jsonResp.get("value").textValue();
return new IDToken(tokenValue);
} catch (IllegalArgumentException e) {
throw new DatabricksException("Received empty ID token from GitHub Actions");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.databricks.sdk.core.oauth;

import com.databricks.sdk.core.CredentialsProvider;
import com.databricks.sdk.core.DatabricksConfig;
import com.databricks.sdk.core.HeaderFactory;
import java.util.HashMap;
import java.util.Map;

/**
* A credentials provider that uses a TokenSource to obtain and manage authentication tokens. This
* class serves as a base implementation for token-based authentication, handling the conversion of
* tokens into HTTP authorization headers.
*
* <p>The provider validates token availability during configuration and creates. appropriate
* authorization headers for API requests.
*/
public class TokenSourceCredentialsProvider implements CredentialsProvider {
private final TokenSource tokenSource;
private final String authType;

/**
* Creates a new TokenSourceCredentialsProvider with the specified token source and auth type.
*
* @param tokenSource The token source responsible for token acquisition and management.
* @param authType The authentication type identifier.
*/
public TokenSourceCredentialsProvider(TokenSource tokenSource, String authType) {
this.tokenSource = tokenSource;
this.authType = authType;
}

/**
* Configures the credentials provider and creates a HeaderFactory for generating authentication
* headers. This method validates token availability by attempting to obtain an access token
* before returning the HeaderFactory.
*
* @param config The Databricks configuration object.
* @return A HeaderFactory that generates "Bearer" token authorization headers, or null if token
* acquisition fails.
*/
@Override
public HeaderFactory configure(DatabricksConfig config) {
try {
// Validate that we can get a token before returning the HeaderFactory
String accessToken = tokenSource.getToken().getAccessToken();

return () -> {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
};
} catch (Exception e) {
return null;
}
}

/**
* Returns the authentication type identifier for this credentials provider. This is used to
* identify the authentication method being used.
*
* @return The authentication type string
*/
@Override
public String authType() {
return authType;
}
}
Loading
Loading