Skip to content
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

SNOW-1831103/SNOW-1853435: Refresh token & OAuth tokens caching support #2009

Merged
merged 10 commits into from
Dec 20, 2024

Conversation

sfc-gh-dheyman
Copy link
Contributor

@sfc-gh-dheyman sfc-gh-dheyman commented Dec 18, 2024

Overview

SNOW-1831103/SNOW-1853435: Refresh token & OAuth tokens caching support

Pre-review self checklist

  • PR branch is updated with all the changes from master branch
  • The code is correctly formatted (run mvn -P check-style validate)
  • New public API is not unnecessary exposed (run mvn verify and inspect target/japicmp/japicmp.html)
  • The pull request name is prefixed with SNOW-XXXX:
  • Code is in compliance with internal logging requirements

External contributors - please answer these questions before submitting a pull request. Thanks!

  1. What GitHub issue is this PR addressing? Make sure that there is an accompanying issue to your PR.

    Issue: #NNNN

  2. Fill out the following pre-review checklist:

    • I am adding a new automated test(s) to verify correctness of my new code
    • I am adding new logging messages
    • I am modifying authorization mechanisms
    • I am adding new credentials
    • I am modifying OCSP code
    • I am adding a new dependency or upgrading an existing one
    • I am adding new public/protected component not marked with @SnowflakeJdbcInternalApi (note that public/protected methods/fields in classes marked with this annotation are already internal)
  3. Please describe how your code solves the related issue.

    Please write a short description of how your code change solves the related issue.

@sfc-gh-dheyman sfc-gh-dheyman requested a review from a team as a code owner December 18, 2024 20:54
logger.debug("Unrecognized type {} for local cached credential", credType);
switch (credType) {
case ID_TOKEN:
logger.debug(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the messages are different only in the part after {} - could we make the login before and once?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -98,6 +104,14 @@ String getMfaToken() {
return mfaToken;
}

public String getOauthAccessToken() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new public methods may be internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed public modifier, it was actually unnecessary

preNewSession(loginInput);
readCachedTokens(loginInput);

if (AccessTokenProviderFactory.isEligible(getAuthenticator(loginInput))) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we extract methods from newly added parts in openSession?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (AccessTokenProviderFactory.isEligible(getAuthenticator(loginInput))) {
if (loginInput.getOauthAccessToken() != null) { // Access Token cached
loginInput.setAuthenticator(AuthenticatorType.OAUTH.name());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will the new authenticators work with session renewal also?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I'm still thinking how to write automated test for it.

TokenResponseDTO tokenResponse = accessTokenProvider.getAccessToken(loginInput);
loginInput.setAuthenticator(AuthenticatorType.OAUTH.name());
loginInput.setToken(tokenResponse.getAccessToken());
loginInput.setOauthAccessToken(tokenResponse.getAccessToken());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we pass access token twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token field is generic token field which is related to /login-request body and oauthToken on the other hand is a field that is being used to store only oauth access token and to interact with token cache

@@ -998,6 +1064,14 @@ public static void deleteMfaTokenCache(String host, String user) {
CredentialManager.getInstance().deleteMfaTokenCache(host, user);
}

private static void deleteOAuthAccessTokenCache(String host, String user) {
CredentialManager.getInstance().deleteOAuthAccessTokenCache(host, user);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cannot we expose static methods directly from CredentialManager? under the hood it's instance could be used and we don't need to provide such helper methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private static final SFLogger logger =
SFLoggerFactory.getLogger(OAuthClientCredentialsAccessTokenProvider.class);

private final ObjectMapper objectMapper = new ObjectMapper();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we want to use ObjectMapperFactory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No really since it's configuration causes problems with deserialization via constructor

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectMapper is thread safe, maybe it could be a static field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to static field.

objectMapper.readValue(tokenResponse, TokenResponseDTO.class);
logger.debug(
"Received new OAuth access token from: {}",
requestUri.getAuthority() + requestUri.getPath());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use separate parameters without concatenation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

try {
cred =
secureStorageManager.getCredential(
loginInput.getHostFromServerUrl(), loginInput.getUserName(), credType);
loginInput.getHostFromServerUrl(), loginInput.getUserName(), credType.getValue());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it is correct for oauth. Guessing from the code above getHostFromServerUrl returns snowflake host, right? For OAuth it should be IDP address. Otherwise we could accidentally leak tokens between IDPs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored according to your suggestion.

}

/** Delete the OAuth access token cache */
static void deleteOAuthAccessTokenCache(String host, String user) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...FromCache?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather leave it as is since another functions are named in similar convention

* @return true if authenticator type is EXTERNALBROWSER
*/
boolean isExternalbrowserAuthenticator() {
boolean isExternalbrowserOrOAuthFullFlowAuthenticator() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full flow is a new name? WDYT about isBrowserBasedAuthenticator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLIENT_CREDENTIALS flow is not browser based.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full flow as in opposed to OAUTH authenticator :)

if (tokenResponse.getRefreshToken() != null) {
loginInput.setOauthRefreshToken(tokenResponse.getRefreshToken());
}
} catch (Throwable e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not catch throwable. What if OOM or stack overflow is thrown here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, fixed.

&& asBoolean(loginInput.getSessionParameters().get(CLIENT_STORE_TEMPORARY_CREDENTIAL))) {
CredentialManager.getInstance().writeIdToken(loginInput, ret);
if (asBoolean(loginInput.getSessionParameters().get(CLIENT_STORE_TEMPORARY_CREDENTIAL))) {
if (consentCacheIdToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the consent is here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, this code was already there. I prefer not to touch it :).

if (consentCacheIdToken) {
CredentialManager.writeIdToken(loginInput, ret);
}
if (loginInput.getOauthAccessToken() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good assertion, but maybe it would be better placed in CredentialManager? It would make the code more robust.

Copy link
Contributor Author

@sfc-gh-dheyman sfc-gh-dheyman Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to leave it here. For me it's counterintuitive - it would suggests that we always save this token

private static final SFLogger logger =
SFLoggerFactory.getLogger(OAuthClientCredentialsAccessTokenProvider.class);

private final ObjectMapper objectMapper = new ObjectMapper();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectMapper is thread safe, maybe it could be a static field?

requestUri.getAuthority(),
requestUri.getPath());
String tokenResponse =
HttpUtil.executeGeneralRequest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use this utility for non-snowflake requests? It contains some of snowflake-specific behaviour, like telemetry, adding request id etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also used in case of external browser saml request. It supports configured retry stategy, timeouts and proxy.

@@ -18,10 +18,10 @@
import net.snowflake.client.log.SFLoggerFactory;

@SnowflakeJdbcInternalApi
public class AccessTokenProviderFactory {
public class OAuthAccessTokenProviderFactory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even more precise - OAuth21, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too detailed. Documentation can point this out but that's about it IMHO.

@sfc-gh-dheyman sfc-gh-dheyman merged commit 0312556 into oauth-code-flow Dec 20, 2024
4 of 7 checks passed
@sfc-gh-dheyman sfc-gh-dheyman deleted the oauth-token-cache branch December 20, 2024 20:44
@github-actions github-actions bot locked and limited conversation to collaborators Dec 20, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants