Skip to content

Commit c092a76

Browse files
Add GitHub OIDC authentication support (#444)
## What changes are proposed in this pull request? This PR adds support for GitHub OIDC (OpenID Connect) authentication in the Databricks SDK Java. ## Key Changes - Added `GithubIDTokenSource` to retrieve JWT tokens from GitHub Actions environment - Introduced new `TokenSourceCredentialsProvider` as a generic provider for token-based authentication flows - Updated `DefaultCredentialsProvider` to support GitHub OIDC and restructured it to facilitate adding other identity providers - Added an example demonstrating GitHub OIDC authentication with GitHub Actions ## How is this tested? - Added unit tests for `GithubIDTokenSource` and `TokenSourceCredentialsProvider` - Manually validated the authentication flow in a GitHub Actions workflow using the provided example NO_CHANGELOG=true --------- Co-authored-by: Renaud Hartert <renaud.hartert@databricks.com>
1 parent 14e4c63 commit c092a76

File tree

8 files changed

+638
-78
lines changed

8 files changed

+638
-78
lines changed

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java

Lines changed: 120 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,69 @@
22

33
import com.databricks.sdk.core.oauth.*;
44
import java.util.ArrayList;
5-
import java.util.Arrays;
65
import java.util.List;
76
import org.slf4j.Logger;
87
import org.slf4j.LoggerFactory;
98

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

13-
private static final List<Class<?>> providerClasses =
14-
Arrays.asList(
15-
PatCredentialsProvider.class,
16-
BasicCredentialsProvider.class,
17-
OAuthM2MServicePrincipalCredentialsProvider.class,
18-
GithubOidcCredentialsProvider.class,
19-
AzureGithubOidcCredentialsProvider.class,
20-
AzureServicePrincipalCredentialsProvider.class,
21-
AzureCliCredentialsProvider.class,
22-
ExternalBrowserCredentialsProvider.class,
23-
DatabricksCliCredentialsProvider.class,
24-
NotebookNativeCredentialsProvider.class,
25-
GoogleCredentialsCredentialsProvider.class,
26-
GoogleIdCredentialsProvider.class);
27-
28-
private final List<CredentialsProvider> providers;
18+
/* List of credential providers that will be tried in sequence */
19+
private List<CredentialsProvider> providers = new ArrayList<>();
2920

21+
/* The currently selected authentication type */
3022
private String authType = "default";
3123

32-
public String authType() {
33-
return authType;
34-
}
24+
/**
25+
* Internal class to associate an ID token source with a name for identification purposes. Used
26+
* primarily for OIDC (OpenID Connect) authentication flows.
27+
*/
28+
private static class NamedIDTokenSource {
29+
private final String name;
30+
private final IDTokenSource idTokenSource;
3531

36-
public DefaultCredentialsProvider() {
37-
providers = new ArrayList<>();
38-
for (Class<?> clazz : providerClasses) {
39-
try {
40-
providers.add((CredentialsProvider) clazz.newInstance());
41-
} catch (NoClassDefFoundError | InstantiationException | IllegalAccessException e) {
42-
LOG.warn(
43-
"Failed to instantiate credentials provider: "
44-
+ clazz.getName()
45-
+ ", skipping. Cause: "
46-
+ e.getClass().getCanonicalName()
47-
+ ": "
48-
+ e.getMessage());
49-
}
32+
public NamedIDTokenSource(String name, IDTokenSource idTokenSource) {
33+
this.name = name;
34+
this.idTokenSource = idTokenSource;
35+
}
36+
37+
public String getName() {
38+
return name;
5039
}
40+
41+
public IDTokenSource getIdTokenSource() {
42+
return idTokenSource;
43+
}
44+
}
45+
46+
public DefaultCredentialsProvider() {}
47+
48+
/**
49+
* Returns the current authentication type being used
50+
*
51+
* @return String representing the authentication type
52+
*/
53+
public String authType() {
54+
return authType;
5155
}
5256

57+
/**
58+
* Configures the credentials provider with the given Databricks configuration. This method tries
59+
* each available credential provider in sequence until one succeeds.
60+
*
61+
* @param config The Databricks configuration containing authentication details
62+
* @return HeaderFactory for making authenticated requests
63+
* @throws DatabricksException if no valid credentials can be configured
64+
*/
5365
@Override
5466
public synchronized HeaderFactory configure(DatabricksConfig config) {
67+
addDefaultCredentialsProviders(config);
5568
for (CredentialsProvider provider : providers) {
5669
if (config.getAuthType() != null
5770
&& !config.getAuthType().isEmpty()
@@ -80,4 +93,77 @@ public synchronized HeaderFactory configure(DatabricksConfig config) {
8093
+ authFlowUrl
8194
+ " to configure credentials for your preferred authentication method");
8295
}
96+
97+
/**
98+
* Adds OpenID Connect (OIDC) based credential providers to the list of available providers.
99+
*
100+
* @param config The Databricks configuration containing OIDC settings
101+
*/
102+
private void addOIDCCredentialsProviders(DatabricksConfig config) {
103+
// TODO: refactor the code so that the IdTokenSources are created within the
104+
// configure call of their corresponding CredentialsProvider. This will allow
105+
// us to simplify the code by validating IdTokenSources when they are created.
106+
OpenIDConnectEndpoints endpoints = null;
107+
try {
108+
endpoints = config.getOidcEndpoints();
109+
} catch (Exception e) {
110+
LOG.warn("Failed to get OpenID Connect endpoints", e);
111+
}
112+
113+
List<NamedIDTokenSource> namedIdTokenSources = new ArrayList<>();
114+
namedIdTokenSources.add(
115+
new NamedIDTokenSource(
116+
"github-oidc",
117+
new GithubIDTokenSource(
118+
config.getActionsIdTokenRequestUrl(),
119+
config.getActionsIdTokenRequestToken(),
120+
config.getHttpClient())));
121+
// Add new IDTokenSources and ID providers here. Example:
122+
// namedIdTokenSources.add(new NamedIDTokenSource("custom-oidc", new CustomIDTokenSource(...)));
123+
124+
// Configure OAuth token sources for each ID token source
125+
for (NamedIDTokenSource namedIdTokenSource : namedIdTokenSources) {
126+
DatabricksOAuthTokenSource oauthTokenSource =
127+
new DatabricksOAuthTokenSource.Builder(
128+
config.getClientId(),
129+
config.getHost(),
130+
endpoints,
131+
namedIdTokenSource.getIdTokenSource(),
132+
config.getHttpClient())
133+
.audience(config.getTokenAudience())
134+
.accountId(config.isAccountClient() ? config.getAccountId() : null)
135+
.build();
136+
137+
providers.add(
138+
new TokenSourceCredentialsProvider(oauthTokenSource, namedIdTokenSource.getName()));
139+
}
140+
}
141+
142+
/**
143+
* Initializes all available credential providers in the preferred order. The order of providers
144+
* determines the authentication fallback sequence.
145+
*
146+
* @param config The Databricks configuration to use for provider initialization
147+
*/
148+
private synchronized void addDefaultCredentialsProviders(DatabricksConfig config) {
149+
if (!providers.isEmpty()) {
150+
return;
151+
}
152+
153+
providers.add(new PatCredentialsProvider());
154+
providers.add(new BasicCredentialsProvider());
155+
providers.add(new OAuthM2MServicePrincipalCredentialsProvider());
156+
157+
// Add OIDC-based providers
158+
addOIDCCredentialsProviders(config);
159+
160+
providers.add(new AzureGithubOidcCredentialsProvider());
161+
providers.add(new AzureServicePrincipalCredentialsProvider());
162+
providers.add(new AzureCliCredentialsProvider());
163+
providers.add(new ExternalBrowserCredentialsProvider());
164+
providers.add(new DatabricksCliCredentialsProvider());
165+
providers.add(new NotebookNativeCredentialsProvider());
166+
providers.add(new GoogleCredentialsCredentialsProvider());
167+
providers.add(new GoogleIdCredentialsProvider());
168+
}
83169
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.databricks.sdk.core.oauth;
2+
3+
import com.databricks.sdk.core.DatabricksException;
4+
import com.databricks.sdk.core.http.HttpClient;
5+
import com.databricks.sdk.core.http.Request;
6+
import com.databricks.sdk.core.http.Response;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.node.ObjectNode;
9+
import com.google.common.base.Strings;
10+
import java.io.IOException;
11+
12+
/**
13+
* GithubIDTokenSource retrieves JWT Tokens from GitHub Actions. This class implements the
14+
* IDTokenSource interface and provides a method for obtaining ID tokens specifically from GitHub
15+
* Actions environment.
16+
*/
17+
public class GithubIDTokenSource implements IDTokenSource {
18+
/* URL endpoint for requesting ID tokens from GitHub Actions */
19+
private final String actionsIDTokenRequestURL;
20+
/* Authentication token required to request ID tokens from GitHub Actions */
21+
private final String actionsIDTokenRequestToken;
22+
/* HTTP client for making requests to GitHub Actions */
23+
private final HttpClient httpClient;
24+
/* JSON mapper for parsing response data */
25+
private static final ObjectMapper mapper = new ObjectMapper();
26+
27+
/**
28+
* Constructs a new GithubIDTokenSource.
29+
*
30+
* @param actionsIDTokenRequestURL The URL to request the ID token from GitHub Actions.
31+
* @param actionsIDTokenRequestToken The token used to authenticate the request.
32+
* @param httpClient The HTTP client to use for making requests.
33+
*/
34+
public GithubIDTokenSource(
35+
String actionsIDTokenRequestURL, String actionsIDTokenRequestToken, HttpClient httpClient) {
36+
this.actionsIDTokenRequestURL = actionsIDTokenRequestURL;
37+
this.actionsIDTokenRequestToken = actionsIDTokenRequestToken;
38+
this.httpClient = httpClient;
39+
}
40+
41+
/**
42+
* Retrieves an ID token from GitHub Actions. This method makes an authenticated request to GitHub
43+
* Actions to obtain a JWT token that later can be exchanged for a Databricks access token.
44+
*
45+
* @param audience Optional audience claim for the token. If provided, it will be included in the
46+
* token request to GitHub Actions.
47+
* @return An IDToken object containing the JWT token value
48+
* @throws DatabricksException if the token request fails or if required configuration is missing
49+
*/
50+
@Override
51+
public IDToken getIDToken(String audience) {
52+
// Validate required configuration
53+
if (Strings.isNullOrEmpty(actionsIDTokenRequestURL)) {
54+
throw new DatabricksException("Missing ActionsIDTokenRequestURL");
55+
}
56+
if (Strings.isNullOrEmpty(actionsIDTokenRequestToken)) {
57+
throw new DatabricksException("Missing ActionsIDTokenRequestToken");
58+
}
59+
if (httpClient == null) {
60+
throw new DatabricksException("HttpClient cannot be null");
61+
}
62+
63+
String requestUrl = actionsIDTokenRequestURL;
64+
if (!Strings.isNullOrEmpty(audience)) {
65+
requestUrl = String.format("%s&audience=%s", requestUrl, audience);
66+
}
67+
68+
Request req =
69+
new Request("GET", requestUrl)
70+
.withHeader("Authorization", "Bearer " + actionsIDTokenRequestToken);
71+
72+
Response resp;
73+
try {
74+
resp = httpClient.execute(req);
75+
} catch (IOException e) {
76+
throw new DatabricksException(
77+
"Failed to request ID token from " + requestUrl + ": " + e.getMessage(), e);
78+
}
79+
80+
// Validate response status code
81+
if (resp.getStatusCode() != 200) {
82+
throw new DatabricksException(
83+
"Failed to request ID token: status code "
84+
+ resp.getStatusCode()
85+
+ ", response body: "
86+
+ resp.getBody().toString());
87+
}
88+
89+
// Parse the JSON response
90+
ObjectNode jsonResp;
91+
try {
92+
jsonResp = mapper.readValue(resp.getBody(), ObjectNode.class);
93+
} catch (IOException e) {
94+
throw new DatabricksException(
95+
"Failed to request ID token: corrupted token: " + e.getMessage());
96+
}
97+
98+
// Validate response structure and token value
99+
if (!jsonResp.has("value")) {
100+
throw new DatabricksException("ID token response missing 'value' field");
101+
}
102+
103+
try {
104+
String tokenValue = jsonResp.get("value").textValue();
105+
return new IDToken(tokenValue);
106+
} catch (IllegalArgumentException e) {
107+
throw new DatabricksException("Received empty ID token from GitHub Actions");
108+
}
109+
}
110+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.databricks.sdk.core.oauth;
2+
3+
import com.databricks.sdk.core.CredentialsProvider;
4+
import com.databricks.sdk.core.DatabricksConfig;
5+
import com.databricks.sdk.core.HeaderFactory;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
/**
10+
* A credentials provider that uses a TokenSource to obtain and manage authentication tokens. This
11+
* class serves as a base implementation for token-based authentication, handling the conversion of
12+
* tokens into HTTP authorization headers.
13+
*
14+
* <p>The provider validates token availability during configuration and creates. appropriate
15+
* authorization headers for API requests.
16+
*/
17+
public class TokenSourceCredentialsProvider implements CredentialsProvider {
18+
private final TokenSource tokenSource;
19+
private final String authType;
20+
21+
/**
22+
* Creates a new TokenSourceCredentialsProvider with the specified token source and auth type.
23+
*
24+
* @param tokenSource The token source responsible for token acquisition and management.
25+
* @param authType The authentication type identifier.
26+
*/
27+
public TokenSourceCredentialsProvider(TokenSource tokenSource, String authType) {
28+
this.tokenSource = tokenSource;
29+
this.authType = authType;
30+
}
31+
32+
/**
33+
* Configures the credentials provider and creates a HeaderFactory for generating authentication
34+
* headers. This method validates token availability by attempting to obtain an access token
35+
* before returning the HeaderFactory.
36+
*
37+
* @param config The Databricks configuration object.
38+
* @return A HeaderFactory that generates "Bearer" token authorization headers, or null if token
39+
* acquisition fails.
40+
*/
41+
@Override
42+
public HeaderFactory configure(DatabricksConfig config) {
43+
try {
44+
// Validate that we can get a token before returning the HeaderFactory
45+
String accessToken = tokenSource.getToken().getAccessToken();
46+
47+
return () -> {
48+
Map<String, String> headers = new HashMap<>();
49+
headers.put("Authorization", "Bearer " + accessToken);
50+
return headers;
51+
};
52+
} catch (Exception e) {
53+
return null;
54+
}
55+
}
56+
57+
/**
58+
* Returns the authentication type identifier for this credentials provider. This is used to
59+
* identify the authentication method being used.
60+
*
61+
* @return The authentication type string
62+
*/
63+
@Override
64+
public String authType() {
65+
return authType;
66+
}
67+
}

0 commit comments

Comments
 (0)