-
Notifications
You must be signed in to change notification settings - Fork 170
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-1831099: OAuth Client Credentials Flow Implementation #1993
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
cbb5810
Initial implementation of client credentials flow
sfc-gh-dheyman a45c797
Add client credentials working flow
sfc-gh-dheyman fd42d99
Reformat
sfc-gh-dheyman 44b90a1
Refactor
sfc-gh-dheyman 2c5896e
CR suggestions
sfc-gh-dheyman 94d2bc7
Remove comment
sfc-gh-dheyman 70a7475
CR suggestions applied
sfc-gh-dheyman 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 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
6 changes: 5 additions & 1 deletion
6
.../auth/oauth/OauthAccessTokenProvider.java → .../core/auth/oauth/AccessTokenProvider.java
This file contains 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 |
---|---|---|
@@ -1,11 +1,15 @@ | ||
/* | ||
* Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. | ||
*/ | ||
|
||
package net.snowflake.client.core.auth.oauth; | ||
|
||
import net.snowflake.client.core.SFException; | ||
import net.snowflake.client.core.SFLoginInput; | ||
import net.snowflake.client.core.SnowflakeJdbcInternalApi; | ||
|
||
@SnowflakeJdbcInternalApi | ||
public interface OauthAccessTokenProvider { | ||
public interface AccessTokenProvider { | ||
|
||
String getAccessToken(SFLoginInput loginInput) throws SFException; | ||
} |
75 changes: 75 additions & 0 deletions
75
src/main/java/net/snowflake/client/core/auth/oauth/AccessTokenProviderFactory.java
This file contains 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,75 @@ | ||
/* | ||
* Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. | ||
*/ | ||
|
||
package net.snowflake.client.core.auth.oauth; | ||
|
||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.Set; | ||
import net.snowflake.client.core.AssertUtil; | ||
import net.snowflake.client.core.SFException; | ||
import net.snowflake.client.core.SFLoginInput; | ||
import net.snowflake.client.core.SessionUtilExternalBrowser; | ||
import net.snowflake.client.core.SnowflakeJdbcInternalApi; | ||
import net.snowflake.client.core.auth.AuthenticatorType; | ||
import net.snowflake.client.jdbc.ErrorCode; | ||
import net.snowflake.client.log.SFLogger; | ||
import net.snowflake.client.log.SFLoggerFactory; | ||
|
||
@SnowflakeJdbcInternalApi | ||
public class AccessTokenProviderFactory { | ||
|
||
private static final SFLogger logger = | ||
SFLoggerFactory.getLogger(AccessTokenProviderFactory.class); | ||
private static final Set<AuthenticatorType> ELIGIBLE_AUTH_TYPES = new HashSet<>(Arrays.asList(AuthenticatorType.OAUTH_AUTHORIZATION_CODE, AuthenticatorType.OAUTH_CLIENT_CREDENTIALS)); | ||
|
||
private final SessionUtilExternalBrowser.AuthExternalBrowserHandlers browserHandler; | ||
private final int browserAuthorizationTimeoutSeconds; | ||
|
||
public AccessTokenProviderFactory( | ||
SessionUtilExternalBrowser.AuthExternalBrowserHandlers browserHandler, | ||
int browserAuthorizationTimeoutSeconds) { | ||
this.browserHandler = browserHandler; | ||
this.browserAuthorizationTimeoutSeconds = browserAuthorizationTimeoutSeconds; | ||
} | ||
|
||
public AccessTokenProvider createAccessTokenProvider( | ||
AuthenticatorType authenticatorType, SFLoginInput loginInput) throws SFException { | ||
switch (authenticatorType) { | ||
case OAUTH_AUTHORIZATION_CODE: | ||
assertContainsClientCredentials(loginInput, authenticatorType); | ||
return new OAuthAuthorizationCodeAccessTokenProvider( | ||
browserHandler, browserAuthorizationTimeoutSeconds); | ||
case OAUTH_CLIENT_CREDENTIALS: | ||
assertContainsClientCredentials(loginInput, authenticatorType); | ||
AssertUtil.assertTrue( | ||
loginInput.getOauthLoginInput().getExternalTokenRequestUrl() != null, | ||
"passing externalTokenRequestUrl is required for OAUTH_CLIENT_CREDENTIALS authentication"); | ||
return new OAuthClientCredentialsAccessTokenProvider(); | ||
default: | ||
logger.error("Unsupported authenticator type: " + authenticatorType); | ||
throw new SFException(ErrorCode.INTERNAL_ERROR); | ||
} | ||
} | ||
|
||
public static Set<AuthenticatorType> getEligible() { | ||
return ELIGIBLE_AUTH_TYPES; | ||
} | ||
|
||
public static boolean isEligible(AuthenticatorType authenticatorType) { | ||
return getEligible().contains(authenticatorType); | ||
} | ||
|
||
private void assertContainsClientCredentials( | ||
SFLoginInput loginInput, AuthenticatorType authenticatorType) throws SFException { | ||
AssertUtil.assertTrue( | ||
loginInput.getOauthLoginInput().getClientId() != null, | ||
String.format( | ||
"passing clientId is required for %s authentication", authenticatorType.name())); | ||
AssertUtil.assertTrue( | ||
loginInput.getOauthLoginInput().getClientSecret() != null, | ||
String.format( | ||
"passing clientSecret is required for %s authentication", authenticatorType.name())); | ||
} | ||
} |
This file contains 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 |
---|---|---|
@@ -1,3 +1,7 @@ | ||
/* | ||
* Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. | ||
*/ | ||
|
||
package net.snowflake.client.core.auth.oauth; | ||
|
||
import static net.snowflake.client.core.SessionUtilExternalBrowser.AuthExternalBrowserHandlers; | ||
|
@@ -14,7 +18,6 @@ | |
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; | ||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; | ||
import com.nimbusds.oauth2.sdk.auth.Secret; | ||
import com.nimbusds.oauth2.sdk.http.HTTPRequest; | ||
import com.nimbusds.oauth2.sdk.id.ClientID; | ||
import com.nimbusds.oauth2.sdk.id.State; | ||
import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; | ||
|
@@ -38,30 +41,23 @@ | |
import net.snowflake.client.log.SFLogger; | ||
import net.snowflake.client.log.SFLoggerFactory; | ||
import org.apache.http.NameValuePair; | ||
import org.apache.http.client.methods.HttpPost; | ||
import org.apache.http.client.methods.HttpRequestBase; | ||
import org.apache.http.client.utils.URLEncodedUtils; | ||
import org.apache.http.entity.StringEntity; | ||
|
||
@SnowflakeJdbcInternalApi | ||
public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessTokenProvider { | ||
public class OAuthAuthorizationCodeAccessTokenProvider implements AccessTokenProvider { | ||
|
||
private static final SFLogger logger = | ||
SFLoggerFactory.getLogger(AuthorizationCodeFlowAccessTokenProvider.class); | ||
|
||
private static final String SNOWFLAKE_AUTHORIZE_ENDPOINT = "/oauth/authorize"; | ||
private static final String SNOWFLAKE_TOKEN_REQUEST_ENDPOINT = "/oauth/token-request"; | ||
SFLoggerFactory.getLogger(OAuthAuthorizationCodeAccessTokenProvider.class); | ||
|
||
private static final String DEFAULT_REDIRECT_HOST = "http://localhost:8001"; | ||
private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; | ||
private static final String DEFAULT_REDIRECT_URI = DEFAULT_REDIRECT_HOST + REDIRECT_URI_ENDPOINT; | ||
public static final String DEFAULT_SESSION_ROLE_SCOPE_PREFIX = "session:role:"; | ||
|
||
private final AuthExternalBrowserHandlers browserHandler; | ||
private final ObjectMapper objectMapper = new ObjectMapper(); | ||
private final int browserAuthorizationTimeoutSeconds; | ||
|
||
public AuthorizationCodeFlowAccessTokenProvider( | ||
public OAuthAuthorizationCodeAccessTokenProvider( | ||
AuthExternalBrowserHandlers browserHandler, int browserAuthorizationTimeoutSeconds) { | ||
this.browserHandler = browserHandler; | ||
this.browserAuthorizationTimeoutSeconds = browserAuthorizationTimeoutSeconds; | ||
|
@@ -70,11 +66,14 @@ public AuthorizationCodeFlowAccessTokenProvider( | |
@Override | ||
public String getAccessToken(SFLoginInput loginInput) throws SFException { | ||
try { | ||
logger.debug("Starting OAuth authorization code authentication flow..."); | ||
CodeVerifier pkceVerifier = new CodeVerifier(); | ||
AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput, pkceVerifier); | ||
return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode, pkceVerifier); | ||
} catch (Exception e) { | ||
logger.error("Error during OAuth authorization code flow", e); | ||
logger.error( | ||
"Error during OAuth authorization code flow. Verify configuration passed to driver and IdP (URLs, grant types, scope, etc.)", | ||
e); | ||
throw new SFException(e, ErrorCode.OAUTH_AUTHORIZATION_CODE_FLOW_ERROR, e.getMessage()); | ||
} | ||
} | ||
|
@@ -98,10 +97,11 @@ private String exchangeAuthorizationCodeForAccessToken( | |
TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); | ||
URI requestUri = request.getEndpointURI(); | ||
logger.debug( | ||
"Requesting access token from: {}", requestUri.getAuthority() + requestUri.getPath()); | ||
"Requesting OAuth access token from: {}", | ||
requestUri.getAuthority() + requestUri.getPath()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here also let's use parameters without concatenation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
String tokenResponse = | ||
HttpUtil.executeGeneralRequest( | ||
convertToBaseRequest(request.toHTTPRequest()), | ||
OAuthUtil.convertToBaseRequest(request.toHTTPRequest()), | ||
loginInput.getLoginTimeout(), | ||
loginInput.getAuthTimeout(), | ||
loginInput.getSocketTimeoutInMillis(), | ||
|
@@ -110,7 +110,9 @@ private String exchangeAuthorizationCodeForAccessToken( | |
TokenResponseDTO tokenResponseDTO = | ||
objectMapper.readValue(tokenResponse, TokenResponseDTO.class); | ||
logger.debug( | ||
"Received OAuth access token from: {}", requestUri.getAuthority() + requestUri.getPath()); | ||
"Received OAuth access token from: {}{}", | ||
requestUri.getAuthority(), | ||
requestUri.getPath()); | ||
return tokenResponseDTO.getAccessToken(); | ||
} catch (Exception e) { | ||
logger.error("Error during making OAuth access token request", e); | ||
|
@@ -128,13 +130,12 @@ private AuthorizationCode letUserAuthorize( | |
browserHandler.openBrowser(authorizeRequestURI.toString()); | ||
String code = codeFuture.get(this.browserAuthorizationTimeoutSeconds, TimeUnit.SECONDS); | ||
return new AuthorizationCode(code); | ||
} catch (TimeoutException e) { | ||
throw new SFException( | ||
e, | ||
ErrorCode.OAUTH_AUTHORIZATION_CODE_FLOW_ERROR, | ||
"Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration."); | ||
} catch (Exception e) { | ||
if (e instanceof TimeoutException) { | ||
throw new SFException( | ||
e, | ||
ErrorCode.OAUTH_AUTHORIZATION_CODE_FLOW_ERROR, | ||
"Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration."); | ||
} | ||
throw new SFException(e, ErrorCode.OAUTH_AUTHORIZATION_CODE_FLOW_ERROR, e.getMessage()); | ||
} finally { | ||
logger.debug("Stopping OAuth redirect URI server @ {}", httpServer.getAddress()); | ||
|
@@ -185,14 +186,15 @@ private static AuthorizationRequest buildAuthorizationRequest( | |
ClientID clientID = new ClientID(oauthLoginInput.getClientId()); | ||
URI callback = buildRedirectUri(oauthLoginInput); | ||
State state = new State(256); | ||
String scope = getScope(loginInput); | ||
String scope = OAuthUtil.getScope(loginInput.getOauthLoginInput(), loginInput.getRole()); | ||
return new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), clientID) | ||
.scope(new Scope(scope)) | ||
.state(state) | ||
.redirectionURI(callback) | ||
.codeChallenge(pkceVerifier, CodeChallengeMethod.S256) | ||
.endpointURI( | ||
getAuthorizationUrl(loginInput.getOauthLoginInput(), loginInput.getServerUrl())) | ||
OAuthUtil.getAuthorizationUrl( | ||
loginInput.getOauthLoginInput(), loginInput.getServerUrl())) | ||
.build(); | ||
} | ||
|
||
|
@@ -205,9 +207,10 @@ private static TokenRequest buildTokenRequest( | |
new ClientSecretBasic( | ||
new ClientID(loginInput.getOauthLoginInput().getClientId()), | ||
new Secret(loginInput.getOauthLoginInput().getClientSecret())); | ||
Scope scope = new Scope(getScope(loginInput)); | ||
Scope scope = | ||
new Scope(OAuthUtil.getScope(loginInput.getOauthLoginInput(), loginInput.getRole())); | ||
return new TokenRequest( | ||
getTokenRequestUrl(loginInput.getOauthLoginInput(), loginInput.getServerUrl()), | ||
OAuthUtil.getTokenRequestUrl(loginInput.getOauthLoginInput(), loginInput.getServerUrl()), | ||
clientAuthentication, | ||
codeGrant, | ||
scope); | ||
|
@@ -220,29 +223,4 @@ private static URI buildRedirectUri(SFOauthLoginInput oauthLoginInput) { | |
: DEFAULT_REDIRECT_URI; | ||
return URI.create(redirectUri); | ||
} | ||
|
||
private static HttpRequestBase convertToBaseRequest(HTTPRequest request) { | ||
HttpPost baseRequest = new HttpPost(request.getURI()); | ||
baseRequest.setEntity(new StringEntity(request.getBody(), StandardCharsets.UTF_8)); | ||
request.getHeaderMap().forEach((key, values) -> baseRequest.addHeader(key, values.get(0))); | ||
return baseRequest; | ||
} | ||
|
||
private static URI getAuthorizationUrl(SFOauthLoginInput oauthLoginInput, String serverUrl) { | ||
return !StringUtils.isNullOrEmpty(oauthLoginInput.getExternalAuthorizationUrl()) | ||
? URI.create(oauthLoginInput.getExternalAuthorizationUrl()) | ||
: URI.create(serverUrl + SNOWFLAKE_AUTHORIZE_ENDPOINT); | ||
} | ||
|
||
private static URI getTokenRequestUrl(SFOauthLoginInput oauthLoginInput, String serverUrl) { | ||
return !StringUtils.isNullOrEmpty(oauthLoginInput.getExternalTokenRequestUrl()) | ||
? URI.create(oauthLoginInput.getExternalTokenRequestUrl()) | ||
: URI.create(serverUrl + SNOWFLAKE_TOKEN_REQUEST_ENDPOINT); | ||
} | ||
|
||
private static String getScope(SFLoginInput loginInput) { | ||
return (!StringUtils.isNullOrEmpty(loginInput.getOauthLoginInput().getScope())) | ||
? loginInput.getOauthLoginInput().getScope() | ||
: DEFAULT_SESSION_ROLE_SCOPE_PREFIX + loginInput.getRole(); | ||
} | ||
} |
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.
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.
internal?
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.
changed.