Skip to content

Commit eb0fad0

Browse files
Adding token cache to u2m oauth
1 parent 3c77bc2 commit eb0fad0

File tree

10 files changed

+679
-12
lines changed

10 files changed

+679
-12
lines changed

databricks-sdk-java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,10 @@
9797
<artifactId>google-auth-library-oauth2-http</artifactId>
9898
<version>1.20.0</version>
9999
</dependency>
100+
<dependency>
101+
<groupId>com.fasterxml.jackson.datatype</groupId>
102+
<artifactId>jackson-datatype-jsr310</artifactId>
103+
<version>${jackson.version}</version>
104+
</dependency>
100105
</dependencies>
101106
</project>

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.databricks.sdk.core.http.Request;
66
import com.databricks.sdk.core.http.Response;
77
import com.databricks.sdk.core.oauth.OpenIDConnectEndpoints;
8+
import com.databricks.sdk.core.oauth.Token;
9+
import com.databricks.sdk.core.oauth.TokenCache;
810
import com.databricks.sdk.core.utils.Cloud;
911
import com.databricks.sdk.core.utils.Environment;
1012
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -38,6 +40,13 @@ public class DatabricksConfig {
3840
@ConfigAttribute(env = "DATABRICKS_REDIRECT_URL", auth = "oauth")
3941
private String redirectUrl;
4042

43+
/**
44+
* The passphrase used to encrypt the OAuth token cache.
45+
* This is optional and only used with token caching.
46+
*/
47+
@ConfigAttribute(env = "DATABRICKS_OAUTH_TOKEN_CACHE_PASSPHRASE", auth = "oauth", sensitive = true)
48+
private String oAuthTokenCachePassphrase;
49+
4150
/**
4251
* The OpenID Connect discovery URL used to retrieve OIDC configuration and endpoints.
4352
*
@@ -141,6 +150,9 @@ public class DatabricksConfig {
141150

142151
private DatabricksEnvironment databricksEnvironment;
143152

153+
// Lazily initialized OAuth token cache
154+
private transient TokenCache tokenCache;
155+
144156
public Environment getEnv() {
145157
return env;
146158
}
@@ -669,4 +681,63 @@ public DatabricksConfig newWithWorkspaceHost(String host) {
669681
"headerFactory"));
670682
return clone(fieldsToSkip).setHost(host);
671683
}
684+
685+
public String getOAuthPassphrase() {
686+
return oAuthTokenCachePassphrase;
687+
}
688+
689+
public DatabricksConfig setOAuthPassphrase(String oAuthPassphrase) {
690+
this.oAuthTokenCachePassphrase = oAuthPassphrase;
691+
return this;
692+
}
693+
694+
/**
695+
* Gets the default OAuth redirect URL.
696+
* If one is not provided explicitly, uses http://localhost:8080/callback
697+
*
698+
* @return The OAuth redirect URL to use
699+
*/
700+
public String getEffectiveOAuthRedirectUrl() {
701+
return redirectUrl != null ? redirectUrl : "http://localhost:8080/callback";
702+
}
703+
704+
/**
705+
* Gets the TokenCache instance for the current configuration.
706+
* Creates it if it doesn't exist yet.
707+
*
708+
* @return A TokenCache instance for the current host, client ID, and scopes
709+
*/
710+
public synchronized TokenCache getTokenCache() {
711+
if (tokenCache == null && getHost() != null && getClientId() != null) {
712+
tokenCache = new TokenCache(getHost(), getClientId(), getScopes(), getOAuthPassphrase());
713+
}
714+
return tokenCache;
715+
}
716+
717+
/**
718+
* Loads a cached token if one exists for the current configuration.
719+
*
720+
* @return The cached token, or null if no valid token is cached
721+
*/
722+
public Token loadCachedToken() {
723+
TokenCache cache = getTokenCache();
724+
if (cache == null) {
725+
return null;
726+
}
727+
return cache.load();
728+
}
729+
730+
/**
731+
* Saves a token to the cache for the current configuration.
732+
*
733+
* @param token The token to cache
734+
* @throws IOException If there is an error saving the token
735+
*/
736+
public void saveTokenToCache(Token token) throws IOException {
737+
TokenCache cache = getTokenCache();
738+
if (cache == null) {
739+
return;
740+
}
741+
cache.save(token);
742+
}
672743
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
import com.databricks.sdk.core.DatabricksException;
66
import com.databricks.sdk.core.HeaderFactory;
77
import java.io.IOException;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
810

911
/**
1012
* A {@code CredentialsProvider} which implements the Authorization Code + PKCE flow by opening a
11-
* browser for the user to authorize the application.
13+
* browser for the user to authorize the application. When cache support is enabled with
14+
* {@link DatabricksConfig#setOAuthPassphrase}, tokens will be cached to avoid repeated authentication.
1215
*/
1316
public class ExternalBrowserCredentialsProvider implements CredentialsProvider {
17+
private static final Logger LOGGER = LoggerFactory.getLogger(ExternalBrowserCredentialsProvider.class);
1418

1519
@Override
1620
public String authType() {
@@ -19,16 +23,53 @@ public String authType() {
1923

2024
@Override
2125
public HeaderFactory configure(DatabricksConfig config) {
22-
if (config.getHost() == null || config.getAuthType() != "external-browser") {
26+
if (config.getHost() == null || config.getClientId() == null || !config.getAuthType().equals("external-browser")) {
2327
return null;
2428
}
29+
2530
try {
26-
OAuthClient client = new OAuthClient(config);
27-
Consent consent = client.initiateConsent();
28-
SessionCredentials creds = consent.launchExternalBrowser();
29-
return creds.configure(config);
31+
// Get the token cache from config
32+
TokenCache tokenCache = config.getTokenCache();
33+
34+
// First try to use the cached token if available
35+
Token cachedToken = tokenCache.load();
36+
if (cachedToken != null && cachedToken.getRefreshToken() != null) {
37+
LOGGER.debug("Found cached token for {}:{}", config.getHost(), config.getClientId());
38+
39+
try {
40+
// Create SessionCredentials with the cached token and try to refresh if needed
41+
SessionCredentials cachedCreds = new SessionCredentials.Builder()
42+
.withToken(cachedToken)
43+
.withHttpClient(config.getHttpClient())
44+
.withClientId(config.getClientId())
45+
.withClientSecret(config.getClientSecret())
46+
.withTokenUrl(config.getOidcEndpoints().getTokenEndpoint())
47+
.withRedirectUrl(config.getEffectiveOAuthRedirectUrl())
48+
.build();
49+
50+
LOGGER.debug("Using cached token (will refresh if needed)");
51+
return cachedCreds.configure(config);
52+
} catch (Exception e) {
53+
// If token refresh fails, log and continue to browser auth
54+
LOGGER.info("Token refresh failed: {}, falling back to browser auth", e.getMessage());
55+
}
56+
}
57+
58+
// If no cached token or refresh failed, perform browser auth
59+
SessionCredentials credentials = performBrowserAuth(config);
60+
tokenCache.save(credentials.getToken());
61+
LOGGER.debug("Saved browser auth token to cache");
62+
return credentials.configure(config);
3063
} catch (IOException | DatabricksException e) {
64+
LOGGER.error("Failed to authenticate: {}", e.getMessage());
3165
return null;
3266
}
3367
}
68+
69+
SessionCredentials performBrowserAuth(DatabricksConfig config) throws IOException {
70+
LOGGER.debug("Performing browser authentication");
71+
OAuthClient client = new OAuthClient(config);
72+
Consent consent = client.initiateConsent();
73+
return consent.launchExternalBrowser();
74+
}
3475
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/OAuthClient.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,7 @@ public OAuthClient(DatabricksConfig config) throws IOException {
9292
.withClientId(config.getClientId())
9393
.withClientSecret(config.getClientSecret())
9494
.withHost(config.getHost())
95-
.withRedirectUrl(
96-
config.getOAuthRedirectUrl() != null
97-
? config.getOAuthRedirectUrl()
98-
: "http://localhost:8080/callback")
95+
.withRedirectUrl(config.getEffectiveOAuthRedirectUrl())
9996
.withScopes(config.getScopes()));
10097
}
10198

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentials.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import java.util.HashMap;
1010
import java.util.Map;
1111
import org.apache.http.HttpHeaders;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
1214

1315
/**
1416
* An implementation of RefreshableTokenSource implementing the refresh_token OAuth grant type.
@@ -20,6 +22,7 @@
2022
public class SessionCredentials extends RefreshableTokenSource
2123
implements CredentialsProvider, Serializable {
2224
private static final long serialVersionUID = 3083941540130596650L;
25+
private static final Logger LOGGER = LoggerFactory.getLogger(SessionCredentials.class);
2326

2427
@Override
2528
public String authType() {
@@ -28,6 +31,8 @@ public String authType() {
2831

2932
@Override
3033
public HeaderFactory configure(DatabricksConfig config) {
34+
this.tokenCache = config.getTokenCache();
35+
3136
return () -> {
3237
Map<String, String> headers = new HashMap<>();
3338
headers.put(
@@ -84,6 +89,7 @@ public SessionCredentials build() {
8489
private final String redirectUrl;
8590
private final String clientId;
8691
private final String clientSecret;
92+
private transient TokenCache tokenCache;
8793

8894
private SessionCredentials(Builder b) {
8995
super(b.token);
@@ -113,7 +119,19 @@ protected Token refresh() {
113119
// cross-origin requests
114120
headers.put("Origin", redirectUrl);
115121
}
116-
return retrieveToken(
122+
Token newToken = retrieveToken(
117123
hc, clientId, clientSecret, tokenUrl, params, headers, AuthParameterPosition.BODY);
124+
125+
// Save the refreshed token directly to cache
126+
if (tokenCache != null) {
127+
try {
128+
tokenCache.save(newToken);
129+
LOGGER.debug("Saved refreshed token to cache");
130+
} catch (Exception e) {
131+
LOGGER.warn("Failed to save token to cache: {}", e.getMessage());
132+
}
133+
}
134+
135+
return newToken;
118136
}
119137
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Token.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.databricks.sdk.core.utils.ClockSupplier;
44
import com.databricks.sdk.core.utils.SystemClockSupplier;
5+
import com.fasterxml.jackson.annotation.JsonCreator;
56
import com.fasterxml.jackson.annotation.JsonProperty;
67
import java.time.LocalDateTime;
78
import java.time.temporal.ChronoUnit;
@@ -37,7 +38,12 @@ public Token(
3738
}
3839

3940
/** Constructor for refreshable tokens. */
40-
public Token(String accessToken, String tokenType, String refreshToken, LocalDateTime expiry) {
41+
@JsonCreator
42+
public Token(
43+
@JsonProperty("accessToken") String accessToken,
44+
@JsonProperty("tokenType") String tokenType,
45+
@JsonProperty("refreshToken") String refreshToken,
46+
@JsonProperty("expiry") LocalDateTime expiry) {
4147
this(accessToken, tokenType, refreshToken, expiry, new SystemClockSupplier());
4248
}
4349

0 commit comments

Comments
 (0)