From 17416800b164cc05d882ce7d860555e223005882 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Sat, 30 Nov 2024 22:25:22 +0100 Subject: [PATCH 01/39] Draft OAuth authz code flow --- parent-pom.xml | 10 ++ .../snowflake/client/core/SFLoginInput.java | 8 +- .../snowflake/client/core/SessionUtil.java | 9 ++ .../client/core/auth/AuthenticatorType.java | 7 +- ...horizationCodeFlowAccessTokenProvider.java | 120 ++++++++++++++++++ .../auth/oauth/OauthAccessTokenProvider.java | 9 ++ thin_public_pom.xml | 5 + 7 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java create mode 100644 src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java diff --git a/parent-pom.xml b/parent-pom.xml index b1742d64e..40a3656dd 100644 --- a/parent-pom.xml +++ b/parent-pom.xml @@ -70,6 +70,7 @@ 4.11.0 4.1.115.Final 9.37.3 + 11.20.1 0.31.1 1.0-alpha-9-stable-1 3.4.2 @@ -218,6 +219,11 @@ nimbus-jose-jwt ${nimbusds.version} + + com.nimbusds + oauth2-oidc-sdk + ${nimbusds.oauth2.version} + com.yammer.metrics metrics-core @@ -645,6 +651,10 @@ com.nimbusds nimbus-jose-jwt + + com.nimbusds + oauth2-oidc-sdk + com.yammer.metrics metrics-core diff --git a/src/main/java/net/snowflake/client/core/SFLoginInput.java b/src/main/java/net/snowflake/client/core/SFLoginInput.java index 5f52b64af..a6c6afd6d 100644 --- a/src/main/java/net/snowflake/client/core/SFLoginInput.java +++ b/src/main/java/net/snowflake/client/core/SFLoginInput.java @@ -160,7 +160,7 @@ public SFLoginInput setAccountName(String accountName) { return this; } - int getLoginTimeout() { + public int getLoginTimeout() { return loginTimeout; } @@ -184,7 +184,7 @@ SFLoginInput setRetryTimeout(int retryTimeout) { return this; } - int getAuthTimeout() { + public int getAuthTimeout() { return authTimeout; } @@ -238,7 +238,7 @@ SFLoginInput setConnectionTimeout(Duration connectionTimeout) { return this; } - int getSocketTimeoutInMillis() { + public int getSocketTimeoutInMillis() { return (int) socketTimeout.toMillis(); } @@ -388,7 +388,7 @@ SFLoginInput setOCSPMode(OCSPMode ocspMode) { return this; } - HttpClientSettingsKey getHttpClientSettingsKey() { + public HttpClientSettingsKey getHttpClientSettingsKey() { return httpClientKey; } diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index e13c21162..17971ad37 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -28,6 +28,8 @@ import net.snowflake.client.core.auth.AuthenticatorType; import net.snowflake.client.core.auth.ClientAuthnDTO; import net.snowflake.client.core.auth.ClientAuthnParameter; +import net.snowflake.client.core.auth.oauth.AuthorizationCodeFlowAccessTokenProvider; +import net.snowflake.client.core.auth.oauth.OauthAccessTokenProvider; import net.snowflake.client.jdbc.ErrorCode; import net.snowflake.client.jdbc.SnowflakeDriver; import net.snowflake.client.jdbc.SnowflakeReauthenticationRequest; @@ -265,6 +267,13 @@ static SFLoginOutput openSession( AssertUtil.assertTrue( loginInput.getLoginTimeout() >= 0, "negative login timeout for opening session"); + if (getAuthenticator(loginInput).equals(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW)) { + OauthAccessTokenProvider accessTokenProvider = new AuthorizationCodeFlowAccessTokenProvider(); + String oauthAccessToken = accessTokenProvider.getAccessToken(loginInput); + loginInput.setAuthenticator(AuthenticatorType.OAUTH.name()); + loginInput.setToken(oauthAccessToken); + } + final AuthenticatorType authenticator = getAuthenticator(loginInput); if (!authenticator.equals(AuthenticatorType.OAUTH)) { // OAuth does not require a username diff --git a/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java b/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java index e25af718a..32d28dd83 100644 --- a/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java +++ b/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java @@ -41,5 +41,10 @@ public enum AuthenticatorType { /* * Authenticator to enable token for regular login with mfa */ - USERNAME_PASSWORD_MFA + USERNAME_PASSWORD_MFA, + + /* + * Authorization code flow with browser popup + */ + OAUTH_AUTHORIZATION_CODE_FLOW } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java new file mode 100644 index 000000000..9f4406e3a --- /dev/null +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -0,0 +1,120 @@ +package net.snowflake.client.core.auth.oauth; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.AuthorizationRequest; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +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.id.ClientID; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.sun.net.httpserver.HttpServer; +import net.snowflake.client.core.HttpUtil; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFLoginInput; +import net.snowflake.client.core.SnowflakeJdbcInternalApi; +import net.snowflake.client.jdbc.ErrorCode; +import org.apache.http.client.methods.HttpGet; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.CompletableFuture; + +@SnowflakeJdbcInternalApi +public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessTokenProvider { + + private static final String REDIRECT_URI_HOST = "localhost"; + private static final int REDIRECT_URI_PORT = 8001; + private static final String REDIRECT_URI_PATH = "/oauth-redirect"; + + @Override + public String getAccessToken(SFLoginInput loginInput) throws SFException { + AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput); + AccessToken accessToken = exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode); + return accessToken.getValue(); + } + + private AuthorizationCode requestAuthorizationCode(SFLoginInput loginInput) throws SFException { + try { + AuthorizationRequest request = buildAuthorizationRequest(loginInput); + + URI requestURI = request.toURI(); + HttpUtil.executeGeneralRequest(new HttpGet(requestURI), + loginInput.getLoginTimeout(), + loginInput.getAuthTimeout(), + loginInput.getSocketTimeoutInMillis(), + 0, + loginInput.getHttpClientSettingsKey()); + CompletableFuture f = getAuthorizationCodeFromRedirectURI(); + f.join(); + return new AuthorizationCode(f.get()); + } catch (Exception e) { + throw new SFException(e, ErrorCode.INTERNAL_ERROR); + } + } + + private static AccessToken exchangeAuthorizationCodeForAccessToken(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws SFException { + try { + TokenRequest request = buildTokenRequest(loginInput, authorizationCode); + TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send()); + if (!response.indicatesSuccess()) { + TokenErrorResponse errorResponse = response.toErrorResponse(); + errorResponse.getErrorObject(); + } + AccessTokenResponse successResponse = response.toSuccessResponse(); + return successResponse.getTokens().getAccessToken(); + } catch (Exception e) { + throw new SFException(e, ErrorCode.INTERNAL_ERROR); + } + } + + private static CompletableFuture getAuthorizationCodeFromRedirectURI() throws IOException { + CompletableFuture accessTokenFuture = new CompletableFuture<>(); + HttpServer httpServer = HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, REDIRECT_URI_PORT), 0); + httpServer.createContext(REDIRECT_URI_PATH, exchange -> { + String authorizationCode = exchange.getRequestURI().getQuery(); + accessTokenFuture.complete(authorizationCode); + httpServer.stop(0); + }); + return accessTokenFuture; + } + + private static AuthorizationRequest buildAuthorizationRequest(SFLoginInput loginInput) throws URISyntaxException { + URI authorizeEndpoint = new URI(String.format("%s/oauth/authorize", loginInput.getServerUrl())); + ClientID clientID = new ClientID("123"); + Scope scope = new Scope("read", "write"); + URI callback = buildRedirectURI(); + State state = new State(); + return new AuthorizationRequest.Builder( + new ResponseType(ResponseType.Value.CODE), clientID) + .scope(scope) + .state(state) + .redirectionURI(callback) + .endpointURI(authorizeEndpoint) + .build(); + } + + private static URI buildRedirectURI() throws URISyntaxException { + return new URI(String.format("https://%s:%s%s", REDIRECT_URI_HOST, REDIRECT_URI_PORT, REDIRECT_URI_PATH)); + } + + private static TokenRequest buildTokenRequest(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws URISyntaxException { + URI callback = buildRedirectURI(); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(authorizationCode, callback); + ClientID clientID = new ClientID("123"); + Secret clientSecret = new Secret("123"); + ClientAuthentication clientAuthentication = new ClientSecretBasic(clientID, clientSecret); + URI tokenEndpoint = new URI(String.format("%s/oauth/token", loginInput.getServerUrl())); + return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, new Scope()); + } +} diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java new file mode 100644 index 000000000..05e9dacc5 --- /dev/null +++ b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java @@ -0,0 +1,9 @@ +package net.snowflake.client.core.auth.oauth; + +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFLoginInput; + +public interface OauthAccessTokenProvider { + + String getAccessToken(SFLoginInput loginInput) throws SFException; +} diff --git a/thin_public_pom.xml b/thin_public_pom.xml index 09c6bf079..eeb42d0f0 100644 --- a/thin_public_pom.xml +++ b/thin_public_pom.xml @@ -194,6 +194,11 @@ nimbus-jose-jwt ${nimbusds.version} + + com.nimbusds + oauth2-oidc-sdk + ${nimbusds.oauth2.version} + com.yammer.metrics metrics-core From 195ff367a638f86287e5e20a582a966087776f64 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Sat, 30 Nov 2024 22:44:59 +0100 Subject: [PATCH 02/39] Refactor --- ...horizationCodeFlowAccessTokenProvider.java | 19 ++++++++----------- .../auth/oauth/OauthAccessTokenProvider.java | 2 ++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index 9f4406e3a..a507d505f 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -47,7 +47,6 @@ public String getAccessToken(SFLoginInput loginInput) throws SFException { private AuthorizationCode requestAuthorizationCode(SFLoginInput loginInput) throws SFException { try { AuthorizationRequest request = buildAuthorizationRequest(loginInput); - URI requestURI = request.toURI(); HttpUtil.executeGeneralRequest(new HttpGet(requestURI), loginInput.getLoginTimeout(), @@ -55,9 +54,8 @@ private AuthorizationCode requestAuthorizationCode(SFLoginInput loginInput) thro loginInput.getSocketTimeoutInMillis(), 0, loginInput.getHttpClientSettingsKey()); - CompletableFuture f = getAuthorizationCodeFromRedirectURI(); - f.join(); - return new AuthorizationCode(f.get()); + String code = getAuthorizationCodeFromRedirectURI().join(); + return new AuthorizationCode(code); } catch (Exception e) { throw new SFException(e, ErrorCode.INTERNAL_ERROR); } @@ -92,9 +90,9 @@ private static CompletableFuture getAuthorizationCodeFromRedirectURI() t private static AuthorizationRequest buildAuthorizationRequest(SFLoginInput loginInput) throws URISyntaxException { URI authorizeEndpoint = new URI(String.format("%s/oauth/authorize", loginInput.getServerUrl())); ClientID clientID = new ClientID("123"); - Scope scope = new Scope("read", "write"); + Scope scope = new Scope(String.format("session:role:%s", loginInput.getRole())); URI callback = buildRedirectURI(); - State state = new State(); + State state = new State(256); return new AuthorizationRequest.Builder( new ResponseType(ResponseType.Value.CODE), clientID) .scope(scope) @@ -111,10 +109,9 @@ private static URI buildRedirectURI() throws URISyntaxException { private static TokenRequest buildTokenRequest(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws URISyntaxException { URI callback = buildRedirectURI(); AuthorizationGrant codeGrant = new AuthorizationCodeGrant(authorizationCode, callback); - ClientID clientID = new ClientID("123"); - Secret clientSecret = new Secret("123"); - ClientAuthentication clientAuthentication = new ClientSecretBasic(clientID, clientSecret); - URI tokenEndpoint = new URI(String.format("%s/oauth/token", loginInput.getServerUrl())); - return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, new Scope()); + ClientAuthentication clientAuthentication = new ClientSecretBasic(new ClientID("123"), new Secret("123")); + URI tokenEndpoint = new URI(String.format("%s/oauth/token-request", loginInput.getServerUrl())); + Scope scope = new Scope("session:role", loginInput.getRole()); + return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, scope); } } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java index 05e9dacc5..60b311953 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java @@ -2,7 +2,9 @@ import net.snowflake.client.core.SFException; import net.snowflake.client.core.SFLoginInput; +import net.snowflake.client.core.SnowflakeJdbcInternalApi; +@SnowflakeJdbcInternalApi public interface OauthAccessTokenProvider { String getAccessToken(SFLoginInput loginInput) throws SFException; From a3dad01fc053c5377b6cfb8d51b1f33ab241df1f Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Mon, 2 Dec 2024 16:16:51 +0100 Subject: [PATCH 03/39] Implement full flow --- .../snowflake/client/core/SFLoginInput.java | 32 +++++ .../net/snowflake/client/core/SFSession.java | 6 + .../client/core/SFSessionProperty.java | 3 + .../snowflake/client/core/SessionUtil.java | 7 +- .../core/SessionUtilExternalBrowser.java | 2 +- ...horizationCodeFlowAccessTokenProvider.java | 132 ++++++++++++------ .../core/auth/oauth/TokenResponseDTO.java | 69 +++++++++ .../snowflake/client/AbstractDriverIT.java | 3 + 8 files changed, 206 insertions(+), 48 deletions(-) create mode 100644 src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java diff --git a/src/main/java/net/snowflake/client/core/SFLoginInput.java b/src/main/java/net/snowflake/client/core/SFLoginInput.java index a6c6afd6d..48292c566 100644 --- a/src/main/java/net/snowflake/client/core/SFLoginInput.java +++ b/src/main/java/net/snowflake/client/core/SFLoginInput.java @@ -54,6 +54,11 @@ public class SFLoginInput { private boolean enableClientStoreTemporaryCredential; private boolean enableClientRequestMfaToken; + //OAuth + private int redirectUriPort = -1; + private String clientId; + private String clientSecret; + private Duration browserResponseTimeout; // Additional headers to add for Snowsight. @@ -417,6 +422,33 @@ SFLoginInput setDisableSamlURLCheck(boolean disableSamlURLCheck) { return this; } + public int getRedirectUriPort() { + return redirectUriPort; + } + + public SFLoginInput setRedirectUriPort(int redirectUriPort) { + this.redirectUriPort = redirectUriPort; + return this; + } + + public String getClientId() { + return clientId; + } + + public SFLoginInput setClientId(String clientId) { + this.clientId = clientId; + return this; + } + + public String getClientSecret() { + return clientSecret; + } + + public SFLoginInput setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + Map getAdditionalHttpHeadersForSnowsight() { return additionalHttpHeadersForSnowsight; } diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index 59aac9d5b..26aee2872 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -671,6 +671,8 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setSessionParameters(sessionParametersMap) .setPrivateKey((PrivateKey) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY)) .setPrivateKeyFile((String) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY_FILE)) + .setClientId((String) connectionPropertiesMap.get(SFSessionProperty.CLIENT_ID)) + .setClientSecret((String) connectionPropertiesMap.get(SFSessionProperty.CLIENT_SECRET)) .setPrivateKeyBase64( (String) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY_BASE64)) .setPrivateKeyPwd( @@ -696,6 +698,10 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setEnableClientRequestMfaToken(enableClientRequestMfaToken) .setBrowserResponseTimeout(browserResponseTimeout); + if (connectionPropertiesMap.containsKey(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)) { + loginInput.setRedirectUriPort((Integer) connectionPropertiesMap.get(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)); + } + logger.info( "Connecting to {} Snowflake domain", loginInput.getHostFromServerUrl().toLowerCase().endsWith(".cn") ? "CHINA" : "GLOBAL"); diff --git a/src/main/java/net/snowflake/client/core/SFSessionProperty.java b/src/main/java/net/snowflake/client/core/SFSessionProperty.java index 97c0adbc2..db9c386e7 100644 --- a/src/main/java/net/snowflake/client/core/SFSessionProperty.java +++ b/src/main/java/net/snowflake/client/core/SFSessionProperty.java @@ -29,6 +29,9 @@ public enum SFSessionProperty { AUTHENTICATOR("authenticator", false, String.class), OKTA_USERNAME("oktausername", false, String.class), PRIVATE_KEY("privateKey", false, PrivateKey.class), + OAUTH_REDIRECT_URI_PORT("oauthRedirectUriPort", false, Integer.class), + CLIENT_ID("clientID", false, String.class), + CLIENT_SECRET("clientSecret", false, String.class), WAREHOUSE("warehouse", false, String.class), LOGIN_TIMEOUT("loginTimeout", false, Integer.class), NETWORK_TIMEOUT("networkTimeout", false, Integer.class), diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index 17971ad37..bdd0a4daa 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -219,8 +219,11 @@ private static AuthenticatorType getAuthenticator(SFLoginInput loginInput) { .equalsIgnoreCase(AuthenticatorType.EXTERNALBROWSER.name())) { // SAML 2.0 compliant service/application return AuthenticatorType.EXTERNALBROWSER; + } else if (loginInput.getAuthenticator().equalsIgnoreCase(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name())) { + // OAuth authorization code flow authentication + return AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW; } else if (loginInput.getAuthenticator().equalsIgnoreCase(AuthenticatorType.OAUTH.name())) { - // OAuth Authentication + // OAuth access code Authentication return AuthenticatorType.OAUTH; } else if (loginInput .getAuthenticator() @@ -268,6 +271,8 @@ static SFLoginOutput openSession( loginInput.getLoginTimeout() >= 0, "negative login timeout for opening session"); if (getAuthenticator(loginInput).equals(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW)) { + AssertUtil.assertTrue(loginInput.getClientId() != null, "passing clientId is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); + AssertUtil.assertTrue(loginInput.getClientSecret() != null, "passing clientSecret is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); OauthAccessTokenProvider accessTokenProvider = new AuthorizationCodeFlowAccessTokenProvider(); String oauthAccessToken = accessTokenProvider.getAccessToken(loginInput); loginInput.setAuthenticator(AuthenticatorType.OAUTH.name()); diff --git a/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java b/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java index 0f83a9642..d15d77799 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java +++ b/src/main/java/net/snowflake/client/core/SessionUtilExternalBrowser.java @@ -61,7 +61,7 @@ public interface AuthExternalBrowserHandlers { void output(String msg); } - static class DefaultAuthExternalBrowserHandlers implements AuthExternalBrowserHandlers { + public static class DefaultAuthExternalBrowserHandlers implements AuthExternalBrowserHandlers { @Override public HttpPost build(URI uri) { return new HttpPost(uri); diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index a507d505f..c87db49a2 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -1,97 +1,123 @@ package net.snowflake.client.core.auth.oauth; -import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.amazonaws.util.StringUtils; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.oauth2.sdk.AuthorizationCode; import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; import com.nimbusds.oauth2.sdk.AuthorizationGrant; import com.nimbusds.oauth2.sdk.AuthorizationRequest; import com.nimbusds.oauth2.sdk.ResponseType; import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.TokenErrorResponse; import com.nimbusds.oauth2.sdk.TokenRequest; -import com.nimbusds.oauth2.sdk.TokenResponse; 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.token.AccessToken; import com.sun.net.httpserver.HttpServer; import net.snowflake.client.core.HttpUtil; import net.snowflake.client.core.SFException; import net.snowflake.client.core.SFLoginInput; import net.snowflake.client.core.SnowflakeJdbcInternalApi; -import net.snowflake.client.jdbc.ErrorCode; -import org.apache.http.client.methods.HttpGet; +import net.snowflake.client.log.SFLogger; +import net.snowflake.client.log.SFLoggerFactory; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static net.snowflake.client.core.SessionUtilExternalBrowser.DefaultAuthExternalBrowserHandlers; @SnowflakeJdbcInternalApi public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessTokenProvider { + private static final SFLogger logger = SFLoggerFactory.getLogger(AuthorizationCodeFlowAccessTokenProvider.class); + + private static final String AUTHORIZE_ENDPOINT = "/oauth/authorize"; + private static final String TOKEN_REQUEST_ENDPOINT = "/oauth/token-request"; + private static final String REDIRECT_URI_HOST = "localhost"; - private static final int REDIRECT_URI_PORT = 8001; - private static final String REDIRECT_URI_PATH = "/oauth-redirect"; + private static final int DEFAULT_REDIRECT_URI_PORT = 8001; + private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; + public static final String SESSION_ROLE_SCOPE = "session:role"; + + public static int AUTHORIZE_REDIRECT_TIMEOUT_MINUTES = 2; + + private final DefaultAuthExternalBrowserHandlers browserUtil = new DefaultAuthExternalBrowserHandlers(); + private final ObjectMapper objectMapper = new ObjectMapper(); @Override public String getAccessToken(SFLoginInput loginInput) throws SFException { AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput); - AccessToken accessToken = exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode); - return accessToken.getValue(); + return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode); } private AuthorizationCode requestAuthorizationCode(SFLoginInput loginInput) throws SFException { try { AuthorizationRequest request = buildAuthorizationRequest(loginInput); - URI requestURI = request.toURI(); - HttpUtil.executeGeneralRequest(new HttpGet(requestURI), - loginInput.getLoginTimeout(), - loginInput.getAuthTimeout(), - loginInput.getSocketTimeoutInMillis(), - 0, - loginInput.getHttpClientSettingsKey()); - String code = getAuthorizationCodeFromRedirectURI().join(); + URI authorizeRequestURI = request.toURI(); + CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(loginInput.getRedirectUriPort()); + letUserAuthorizeViaBrowser(authorizeRequestURI); + String code = codeFuture.get(AUTHORIZE_REDIRECT_TIMEOUT_MINUTES, TimeUnit.MINUTES); return new AuthorizationCode(code); } catch (Exception e) { - throw new SFException(e, ErrorCode.INTERNAL_ERROR); + if (e instanceof TimeoutException) { + logger.error("Authorization request timed out. Did not receive authorization code back to the redirect URI"); + } + throw new RuntimeException(e.getMessage(), e); } } - private static AccessToken exchangeAuthorizationCodeForAccessToken(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws SFException { + private String exchangeAuthorizationCodeForAccessToken(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws SFException { try { TokenRequest request = buildTokenRequest(loginInput, authorizationCode); - TokenResponse response = TokenResponse.parse(request.toHTTPRequest().send()); - if (!response.indicatesSuccess()) { - TokenErrorResponse errorResponse = response.toErrorResponse(); - errorResponse.getErrorObject(); - } - AccessTokenResponse successResponse = response.toSuccessResponse(); - return successResponse.getTokens().getAccessToken(); + String tokenResponse = HttpUtil.executeGeneralRequest( + convertTokenRequest(request.toHTTPRequest()), + loginInput.getLoginTimeout(), + loginInput.getAuthTimeout(), + loginInput.getSocketTimeoutInMillis(), + 0, + loginInput.getHttpClientSettingsKey()); + TokenResponseDTO tokenResponseDTO = objectMapper.readValue(tokenResponse, TokenResponseDTO.class); + return tokenResponseDTO.getAccessToken(); } catch (Exception e) { - throw new SFException(e, ErrorCode.INTERNAL_ERROR); + throw new RuntimeException(e); } } - private static CompletableFuture getAuthorizationCodeFromRedirectURI() throws IOException { + private void letUserAuthorizeViaBrowser(URI authorizeRequestURI) throws SFException { + browserUtil.openBrowser(authorizeRequestURI.toString()); + } + + private static CompletableFuture setupRedirectURIServerForAuthorizationCode(int redirectUriPort) throws IOException { CompletableFuture accessTokenFuture = new CompletableFuture<>(); - HttpServer httpServer = HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, REDIRECT_URI_PORT), 0); - httpServer.createContext(REDIRECT_URI_PATH, exchange -> { - String authorizationCode = exchange.getRequestURI().getQuery(); - accessTokenFuture.complete(authorizationCode); - httpServer.stop(0); + int redirectPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; + HttpServer httpServer = HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, redirectPort), 0); + httpServer.createContext(REDIRECT_URI_ENDPOINT, exchange -> { + String authorizationCode = extractAuthorizationCodeFromQueryParameters(exchange.getRequestURI().getQuery()); + if (!StringUtils.isNullOrEmpty(authorizationCode)) { + accessTokenFuture.complete(authorizationCode); + httpServer.stop(0); + } }); + httpServer.start(); return accessTokenFuture; } private static AuthorizationRequest buildAuthorizationRequest(SFLoginInput loginInput) throws URISyntaxException { - URI authorizeEndpoint = new URI(String.format("%s/oauth/authorize", loginInput.getServerUrl())); - ClientID clientID = new ClientID("123"); - Scope scope = new Scope(String.format("session:role:%s", loginInput.getRole())); - URI callback = buildRedirectURI(); + URI authorizeEndpoint = new URI(loginInput.getServerUrl() + AUTHORIZE_ENDPOINT); + ClientID clientID = new ClientID(loginInput.getClientId()); + Scope scope = new Scope(String.format("%s:%s", SESSION_ROLE_SCOPE, loginInput.getRole())); + URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); State state = new State(256); return new AuthorizationRequest.Builder( new ResponseType(ResponseType.Value.CODE), clientID) @@ -102,16 +128,30 @@ private static AuthorizationRequest buildAuthorizationRequest(SFLoginInput login .build(); } - private static URI buildRedirectURI() throws URISyntaxException { - return new URI(String.format("https://%s:%s%s", REDIRECT_URI_HOST, REDIRECT_URI_PORT, REDIRECT_URI_PATH)); - } - private static TokenRequest buildTokenRequest(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws URISyntaxException { - URI callback = buildRedirectURI(); + URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); AuthorizationGrant codeGrant = new AuthorizationCodeGrant(authorizationCode, callback); - ClientAuthentication clientAuthentication = new ClientSecretBasic(new ClientID("123"), new Secret("123")); - URI tokenEndpoint = new URI(String.format("%s/oauth/token-request", loginInput.getServerUrl())); - Scope scope = new Scope("session:role", loginInput.getRole()); + ClientAuthentication clientAuthentication = new ClientSecretBasic(new ClientID(loginInput.getClientId()), new Secret(loginInput.getClientSecret())); + URI tokenEndpoint = new URI(String.format(loginInput.getServerUrl() + TOKEN_REQUEST_ENDPOINT)); + Scope scope = new Scope(SESSION_ROLE_SCOPE, loginInput.getRole()); return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, scope); } + + private static URI buildRedirectURI(int redirectUriPort) throws URISyntaxException { + redirectUriPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; + return new URI(String.format("http://%s:%s%s", REDIRECT_URI_HOST, redirectUriPort, REDIRECT_URI_ENDPOINT)); + } + + private static String extractAuthorizationCodeFromQueryParameters(String queryParameters) { + String prefix = "code="; + String codeSuffix = queryParameters.substring(queryParameters.indexOf(prefix) + prefix.length()); + return codeSuffix.substring(0, codeSuffix.indexOf("&")); + } + + private static HttpRequestBase convertTokenRequest(HTTPRequest nimbusRequest) { + HttpPost request = new HttpPost(nimbusRequest.getURI()); + request.setEntity(new StringEntity(nimbusRequest.getBody(), StandardCharsets.UTF_8)); + nimbusRequest.getHeaderMap().forEach((key, values) -> request.addHeader(key, values.get(0))); + return request; + } } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java b/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java new file mode 100644 index 000000000..6d00bd84c --- /dev/null +++ b/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java @@ -0,0 +1,69 @@ +package net.snowflake.client.core.auth.oauth; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +class TokenResponseDTO { + + private final String accessToken; + private final String refreshToken; + private final String tokenType; + private final String scope; + private final String username; + private final boolean idpInitiated; + private final long expiresIn; + private final long refreshTokenExpiresIn; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TokenResponseDTO(@JsonProperty("access_token") String accessToken, + @JsonProperty("refresh_token") String refreshToken, + @JsonProperty("token_type") String tokenType, + @JsonProperty("scope") String scope, + @JsonProperty("username") String username, + @JsonProperty("idp_initiated") boolean idpInitiated, + @JsonProperty("expires_in") long expiresIn, + @JsonProperty("refresh_token_expires_in") long refreshTokenExpiresIn) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.refreshToken = refreshToken; + this.scope = scope; + this.username = username; + this.idpInitiated = idpInitiated; + this.expiresIn = expiresIn; + this.refreshTokenExpiresIn = refreshTokenExpiresIn; + } + + public String getAccessToken() { + return accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public String getRefreshToken() { + return refreshToken; + } + + public String getScope() { + return scope; + } + + public long getExpiresIn() { + return expiresIn; + } + + public String getUsername() { + return username; + } + + public long getRefreshTokenExpiresIn() { + return refreshTokenExpiresIn; + } + + public boolean isIdpInitiated() { + return idpInitiated; + } +} diff --git a/src/test/java/net/snowflake/client/AbstractDriverIT.java b/src/test/java/net/snowflake/client/AbstractDriverIT.java index 3104ce7e9..c370cd8ad 100644 --- a/src/test/java/net/snowflake/client/AbstractDriverIT.java +++ b/src/test/java/net/snowflake/client/AbstractDriverIT.java @@ -6,6 +6,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import com.google.common.base.Strings; +import net.snowflake.client.core.auth.AuthenticatorType; + import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; @@ -323,6 +325,7 @@ public static Connection getConnection( properties.put("internal", Boolean.TRUE.toString()); // TODO: do we need this? properties.put("insecureMode", false); // use OCSP for all tests. + properties.put("authenticator", AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name()); if (injectSocketTimeout > 0) { properties.put("injectSocketTimeout", String.valueOf(injectSocketTimeout)); From 0982aaf02165ca1ad45c25367468a6a2733c18c1 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Mon, 2 Dec 2024 23:56:15 +0100 Subject: [PATCH 04/39] Add wiremock test --- .../snowflake/client/core/SFLoginInput.java | 10 +- .../net/snowflake/client/core/SFSession.java | 3 +- .../snowflake/client/core/SessionUtil.java | 16 +- ...horizationCodeFlowAccessTokenProvider.java | 276 ++++++++++-------- .../auth/oauth/OauthAccessTokenProvider.java | 2 +- .../core/auth/oauth/TokenResponseDTO.java | 103 ++++--- .../snowflake/client/AbstractDriverIT.java | 5 +- .../client/jdbc/BaseWiremockTest.java | 4 +- .../OauthAuthorizationCodeFlowLatestIT.java | 136 +++++++++ 9 files changed, 368 insertions(+), 187 deletions(-) create mode 100644 src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java diff --git a/src/main/java/net/snowflake/client/core/SFLoginInput.java b/src/main/java/net/snowflake/client/core/SFLoginInput.java index 48292c566..0b9fe94cd 100644 --- a/src/main/java/net/snowflake/client/core/SFLoginInput.java +++ b/src/main/java/net/snowflake/client/core/SFLoginInput.java @@ -54,7 +54,7 @@ public class SFLoginInput { private boolean enableClientStoreTemporaryCredential; private boolean enableClientRequestMfaToken; - //OAuth + // OAuth private int redirectUriPort = -1; private String clientId; private String clientSecret; @@ -64,7 +64,7 @@ public class SFLoginInput { // Additional headers to add for Snowsight. Map additionalHttpHeadersForSnowsight; - SFLoginInput() {} + public SFLoginInput() {} Duration getBrowserResponseTimeout() { return browserResponseTimeout; @@ -79,7 +79,7 @@ public String getServerUrl() { return serverUrl; } - SFLoginInput setServerUrl(String serverUrl) { + public SFLoginInput setServerUrl(String serverUrl) { this.serverUrl = serverUrl; return this; } @@ -247,7 +247,7 @@ public int getSocketTimeoutInMillis() { return (int) socketTimeout.toMillis(); } - SFLoginInput setSocketTimeout(Duration socketTimeout) { + public SFLoginInput setSocketTimeout(Duration socketTimeout) { this.socketTimeout = socketTimeout; return this; } @@ -397,7 +397,7 @@ public HttpClientSettingsKey getHttpClientSettingsKey() { return httpClientKey; } - SFLoginInput setHttpClientSettingsKey(HttpClientSettingsKey key) { + public SFLoginInput setHttpClientSettingsKey(HttpClientSettingsKey key) { this.httpClientKey = key; return this; } diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index 26aee2872..a9b969772 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -699,7 +699,8 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setBrowserResponseTimeout(browserResponseTimeout); if (connectionPropertiesMap.containsKey(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)) { - loginInput.setRedirectUriPort((Integer) connectionPropertiesMap.get(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)); + loginInput.setRedirectUriPort( + (Integer) connectionPropertiesMap.get(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)); } logger.info( diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index bdd0a4daa..3a52b02e4 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -219,7 +219,9 @@ private static AuthenticatorType getAuthenticator(SFLoginInput loginInput) { .equalsIgnoreCase(AuthenticatorType.EXTERNALBROWSER.name())) { // SAML 2.0 compliant service/application return AuthenticatorType.EXTERNALBROWSER; - } else if (loginInput.getAuthenticator().equalsIgnoreCase(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name())) { + } else if (loginInput + .getAuthenticator() + .equalsIgnoreCase(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name())) { // OAuth authorization code flow authentication return AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW; } else if (loginInput.getAuthenticator().equalsIgnoreCase(AuthenticatorType.OAUTH.name())) { @@ -271,9 +273,15 @@ static SFLoginOutput openSession( loginInput.getLoginTimeout() >= 0, "negative login timeout for opening session"); if (getAuthenticator(loginInput).equals(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW)) { - AssertUtil.assertTrue(loginInput.getClientId() != null, "passing clientId is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); - AssertUtil.assertTrue(loginInput.getClientSecret() != null, "passing clientSecret is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); - OauthAccessTokenProvider accessTokenProvider = new AuthorizationCodeFlowAccessTokenProvider(); + AssertUtil.assertTrue( + loginInput.getClientId() != null, + "passing clientId is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); + AssertUtil.assertTrue( + loginInput.getClientSecret() != null, + "passing clientSecret is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); + OauthAccessTokenProvider accessTokenProvider = + new AuthorizationCodeFlowAccessTokenProvider( + new SessionUtilExternalBrowser.DefaultAuthExternalBrowserHandlers()); String oauthAccessToken = accessTokenProvider.getAccessToken(loginInput); loginInput.setAuthenticator(AuthenticatorType.OAUTH.name()); loginInput.setToken(oauthAccessToken); diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index c87db49a2..32e27e844 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -1,5 +1,7 @@ package net.snowflake.client.core.auth.oauth; +import static net.snowflake.client.core.SessionUtilExternalBrowser.*; + import com.amazonaws.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.oauth2.sdk.AuthorizationCode; @@ -15,7 +17,17 @@ 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; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import net.snowflake.client.core.HttpUtil; import net.snowflake.client.core.SFException; import net.snowflake.client.core.SFLoginInput; @@ -26,132 +38,154 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static net.snowflake.client.core.SessionUtilExternalBrowser.DefaultAuthExternalBrowserHandlers; - @SnowflakeJdbcInternalApi public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessTokenProvider { - private static final SFLogger logger = SFLoggerFactory.getLogger(AuthorizationCodeFlowAccessTokenProvider.class); - - private static final String AUTHORIZE_ENDPOINT = "/oauth/authorize"; - private static final String TOKEN_REQUEST_ENDPOINT = "/oauth/token-request"; - - private static final String REDIRECT_URI_HOST = "localhost"; - private static final int DEFAULT_REDIRECT_URI_PORT = 8001; - private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; - public static final String SESSION_ROLE_SCOPE = "session:role"; - - public static int AUTHORIZE_REDIRECT_TIMEOUT_MINUTES = 2; - - private final DefaultAuthExternalBrowserHandlers browserUtil = new DefaultAuthExternalBrowserHandlers(); - private final ObjectMapper objectMapper = new ObjectMapper(); - - @Override - public String getAccessToken(SFLoginInput loginInput) throws SFException { - AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput); - return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode); - } - - private AuthorizationCode requestAuthorizationCode(SFLoginInput loginInput) throws SFException { - try { - AuthorizationRequest request = buildAuthorizationRequest(loginInput); - URI authorizeRequestURI = request.toURI(); - CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(loginInput.getRedirectUriPort()); - letUserAuthorizeViaBrowser(authorizeRequestURI); - String code = codeFuture.get(AUTHORIZE_REDIRECT_TIMEOUT_MINUTES, TimeUnit.MINUTES); - return new AuthorizationCode(code); - } catch (Exception e) { - if (e instanceof TimeoutException) { - logger.error("Authorization request timed out. Did not receive authorization code back to the redirect URI"); - } - throw new RuntimeException(e.getMessage(), e); - } + 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"; + + private static final String REDIRECT_URI_HOST = "localhost"; + private static final int DEFAULT_REDIRECT_URI_PORT = 8001; + private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; + public static final String SESSION_ROLE_SCOPE = "session:role"; + + public static int AUTHORIZE_REDIRECT_TIMEOUT_MINUTES = 2; + + private final AuthExternalBrowserHandlers browserHandler; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public AuthorizationCodeFlowAccessTokenProvider(AuthExternalBrowserHandlers browserHandler) { + this.browserHandler = browserHandler; + } + + @Override + public String getAccessToken(SFLoginInput loginInput) throws SFException { + CodeVerifier pkceVerifier = new CodeVerifier(); + AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput, pkceVerifier); + return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode, pkceVerifier); + } + + private AuthorizationCode requestAuthorizationCode( + SFLoginInput loginInput, CodeVerifier pkceVerifier) throws SFException { + try { + AuthorizationRequest request = buildAuthorizationRequest(loginInput, pkceVerifier); + URI authorizeRequestURI = request.toURI(); + CompletableFuture codeFuture = + setupRedirectURIServerForAuthorizationCode(loginInput.getRedirectUriPort()); + letUserAuthorizeViaBrowser(authorizeRequestURI); + String code = codeFuture.get(AUTHORIZE_REDIRECT_TIMEOUT_MINUTES, TimeUnit.MINUTES); + return new AuthorizationCode(code); + } catch (Exception e) { + if (e instanceof TimeoutException) { + logger.error( + "Authorization request timed out. Did not receive authorization code back to the redirect URI"); + } + throw new RuntimeException(e); } - - private String exchangeAuthorizationCodeForAccessToken(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws SFException { - try { - TokenRequest request = buildTokenRequest(loginInput, authorizationCode); - String tokenResponse = HttpUtil.executeGeneralRequest( - convertTokenRequest(request.toHTTPRequest()), - loginInput.getLoginTimeout(), - loginInput.getAuthTimeout(), - loginInput.getSocketTimeoutInMillis(), - 0, - loginInput.getHttpClientSettingsKey()); - TokenResponseDTO tokenResponseDTO = objectMapper.readValue(tokenResponse, TokenResponseDTO.class); - return tokenResponseDTO.getAccessToken(); - } catch (Exception e) { - throw new RuntimeException(e); - } + } + + private String exchangeAuthorizationCodeForAccessToken( + SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) + throws SFException { + try { + TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); + String tokenResponse = + HttpUtil.executeGeneralRequest( + convertTokenRequest(request.toHTTPRequest()), + loginInput.getLoginTimeout(), + loginInput.getAuthTimeout(), + loginInput.getSocketTimeoutInMillis(), + 0, + loginInput.getHttpClientSettingsKey()); + TokenResponseDTO tokenResponseDTO = + objectMapper.readValue(tokenResponse, TokenResponseDTO.class); + return tokenResponseDTO.getAccessToken(); + } catch (Exception e) { + throw new RuntimeException(e); } - - private void letUserAuthorizeViaBrowser(URI authorizeRequestURI) throws SFException { - browserUtil.openBrowser(authorizeRequestURI.toString()); - } - - private static CompletableFuture setupRedirectURIServerForAuthorizationCode(int redirectUriPort) throws IOException { - CompletableFuture accessTokenFuture = new CompletableFuture<>(); - int redirectPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; - HttpServer httpServer = HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, redirectPort), 0); - httpServer.createContext(REDIRECT_URI_ENDPOINT, exchange -> { - String authorizationCode = extractAuthorizationCodeFromQueryParameters(exchange.getRequestURI().getQuery()); - if (!StringUtils.isNullOrEmpty(authorizationCode)) { - accessTokenFuture.complete(authorizationCode); - httpServer.stop(0); - } + } + + private void letUserAuthorizeViaBrowser(URI authorizeRequestURI) throws SFException { + browserHandler.openBrowser(authorizeRequestURI.toString()); + } + + private static CompletableFuture setupRedirectURIServerForAuthorizationCode( + int redirectUriPort) throws IOException { + CompletableFuture accessTokenFuture = new CompletableFuture<>(); + int redirectPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; + HttpServer httpServer = + HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, redirectPort), 0); + httpServer.createContext( + REDIRECT_URI_ENDPOINT, + exchange -> { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + String authorizationCode = + extractAuthorizationCodeFromQueryParameters(exchange.getRequestURI().getQuery()); + if (!StringUtils.isNullOrEmpty(authorizationCode)) { + accessTokenFuture.complete(authorizationCode); + httpServer.stop(0); + } }); - httpServer.start(); - return accessTokenFuture; - } - - private static AuthorizationRequest buildAuthorizationRequest(SFLoginInput loginInput) throws URISyntaxException { - URI authorizeEndpoint = new URI(loginInput.getServerUrl() + AUTHORIZE_ENDPOINT); - ClientID clientID = new ClientID(loginInput.getClientId()); - Scope scope = new Scope(String.format("%s:%s", SESSION_ROLE_SCOPE, loginInput.getRole())); - URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); - State state = new State(256); - return new AuthorizationRequest.Builder( - new ResponseType(ResponseType.Value.CODE), clientID) - .scope(scope) - .state(state) - .redirectionURI(callback) - .endpointURI(authorizeEndpoint) - .build(); - } - - private static TokenRequest buildTokenRequest(SFLoginInput loginInput, AuthorizationCode authorizationCode) throws URISyntaxException { - URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); - AuthorizationGrant codeGrant = new AuthorizationCodeGrant(authorizationCode, callback); - ClientAuthentication clientAuthentication = new ClientSecretBasic(new ClientID(loginInput.getClientId()), new Secret(loginInput.getClientSecret())); - URI tokenEndpoint = new URI(String.format(loginInput.getServerUrl() + TOKEN_REQUEST_ENDPOINT)); - Scope scope = new Scope(SESSION_ROLE_SCOPE, loginInput.getRole()); - return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, scope); - } - - private static URI buildRedirectURI(int redirectUriPort) throws URISyntaxException { - redirectUriPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; - return new URI(String.format("http://%s:%s%s", REDIRECT_URI_HOST, redirectUriPort, REDIRECT_URI_ENDPOINT)); - } - - private static String extractAuthorizationCodeFromQueryParameters(String queryParameters) { - String prefix = "code="; - String codeSuffix = queryParameters.substring(queryParameters.indexOf(prefix) + prefix.length()); - return codeSuffix.substring(0, codeSuffix.indexOf("&")); - } - - private static HttpRequestBase convertTokenRequest(HTTPRequest nimbusRequest) { - HttpPost request = new HttpPost(nimbusRequest.getURI()); - request.setEntity(new StringEntity(nimbusRequest.getBody(), StandardCharsets.UTF_8)); - nimbusRequest.getHeaderMap().forEach((key, values) -> request.addHeader(key, values.get(0))); - return request; + httpServer.start(); + return accessTokenFuture; + } + + private static AuthorizationRequest buildAuthorizationRequest( + SFLoginInput loginInput, CodeVerifier pkceVerifier) throws URISyntaxException { + URI authorizeEndpoint = new URI(loginInput.getServerUrl() + SNOWFLAKE_AUTHORIZE_ENDPOINT); + ClientID clientID = new ClientID(loginInput.getClientId()); + Scope scope = new Scope(String.format("%s:%s", SESSION_ROLE_SCOPE, loginInput.getRole())); + URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); + State state = new State(256); + return new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), clientID) + .scope(scope) + .state(state) + .redirectionURI(callback) + .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) + .endpointURI(authorizeEndpoint) + .build(); + } + + private static TokenRequest buildTokenRequest( + SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) + throws URISyntaxException { + URI redirectURI = buildRedirectURI(loginInput.getRedirectUriPort()); + AuthorizationGrant codeGrant = + new AuthorizationCodeGrant(authorizationCode, redirectURI, pkceVerifier); + ClientAuthentication clientAuthentication = + new ClientSecretBasic( + new ClientID(loginInput.getClientId()), new Secret(loginInput.getClientSecret())); + URI tokenEndpoint = + new URI(String.format(loginInput.getServerUrl() + SNOWFLAKE_TOKEN_REQUEST_ENDPOINT)); + Scope scope = new Scope(SESSION_ROLE_SCOPE, loginInput.getRole()); + return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, scope); + } + + private static URI buildRedirectURI(int redirectUriPort) throws URISyntaxException { + redirectUriPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; + return new URI( + String.format("http://%s:%s%s", REDIRECT_URI_HOST, redirectUriPort, REDIRECT_URI_ENDPOINT)); + } + + private static String extractAuthorizationCodeFromQueryParameters(String queryParameters) { + String prefix = "code="; + String codeSuffix = + queryParameters.substring(queryParameters.indexOf(prefix) + prefix.length()); + if (codeSuffix.contains("&")) { + return codeSuffix.substring(0, codeSuffix.indexOf("&")); + } else { + return codeSuffix; } + } + + private static HttpRequestBase convertTokenRequest(HTTPRequest nimbusRequest) { + HttpPost request = new HttpPost(nimbusRequest.getURI()); + request.setEntity(new StringEntity(nimbusRequest.getBody(), StandardCharsets.UTF_8)); + nimbusRequest.getHeaderMap().forEach((key, values) -> request.addHeader(key, values.get(0))); + return request; + } } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java index 60b311953..713e6a282 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/OauthAccessTokenProvider.java @@ -7,5 +7,5 @@ @SnowflakeJdbcInternalApi public interface OauthAccessTokenProvider { - String getAccessToken(SFLoginInput loginInput) throws SFException; + String getAccessToken(SFLoginInput loginInput) throws SFException; } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java b/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java index 6d00bd84c..3db842276 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/TokenResponseDTO.java @@ -1,69 +1,68 @@ package net.snowflake.client.core.auth.oauth; - import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; class TokenResponseDTO { - private final String accessToken; - private final String refreshToken; - private final String tokenType; - private final String scope; - private final String username; - private final boolean idpInitiated; - private final long expiresIn; - private final long refreshTokenExpiresIn; + private final String accessToken; + private final String refreshToken; + private final String tokenType; + private final String scope; + private final String username; + private final boolean idpInitiated; + private final long expiresIn; + private final long refreshTokenExpiresIn; - @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public TokenResponseDTO(@JsonProperty("access_token") String accessToken, - @JsonProperty("refresh_token") String refreshToken, - @JsonProperty("token_type") String tokenType, - @JsonProperty("scope") String scope, - @JsonProperty("username") String username, - @JsonProperty("idp_initiated") boolean idpInitiated, - @JsonProperty("expires_in") long expiresIn, - @JsonProperty("refresh_token_expires_in") long refreshTokenExpiresIn) { - this.accessToken = accessToken; - this.tokenType = tokenType; - this.refreshToken = refreshToken; - this.scope = scope; - this.username = username; - this.idpInitiated = idpInitiated; - this.expiresIn = expiresIn; - this.refreshTokenExpiresIn = refreshTokenExpiresIn; - } + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public TokenResponseDTO( + @JsonProperty("access_token") String accessToken, + @JsonProperty("refresh_token") String refreshToken, + @JsonProperty("token_type") String tokenType, + @JsonProperty("scope") String scope, + @JsonProperty("username") String username, + @JsonProperty("idp_initiated") boolean idpInitiated, + @JsonProperty("expires_in") long expiresIn, + @JsonProperty("refresh_token_expires_in") long refreshTokenExpiresIn) { + this.accessToken = accessToken; + this.tokenType = tokenType; + this.refreshToken = refreshToken; + this.scope = scope; + this.username = username; + this.idpInitiated = idpInitiated; + this.expiresIn = expiresIn; + this.refreshTokenExpiresIn = refreshTokenExpiresIn; + } - public String getAccessToken() { - return accessToken; - } + public String getAccessToken() { + return accessToken; + } - public String getTokenType() { - return tokenType; - } + public String getTokenType() { + return tokenType; + } - public String getRefreshToken() { - return refreshToken; - } + public String getRefreshToken() { + return refreshToken; + } - public String getScope() { - return scope; - } + public String getScope() { + return scope; + } - public long getExpiresIn() { - return expiresIn; - } + public long getExpiresIn() { + return expiresIn; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public long getRefreshTokenExpiresIn() { - return refreshTokenExpiresIn; - } + public long getRefreshTokenExpiresIn() { + return refreshTokenExpiresIn; + } - public boolean isIdpInitiated() { - return idpInitiated; - } + public boolean isIdpInitiated() { + return idpInitiated; + } } diff --git a/src/test/java/net/snowflake/client/AbstractDriverIT.java b/src/test/java/net/snowflake/client/AbstractDriverIT.java index c370cd8ad..113fedeee 100644 --- a/src/test/java/net/snowflake/client/AbstractDriverIT.java +++ b/src/test/java/net/snowflake/client/AbstractDriverIT.java @@ -6,8 +6,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import com.google.common.base.Strings; -import net.snowflake.client.core.auth.AuthenticatorType; - import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; @@ -26,6 +24,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; +import net.snowflake.client.core.auth.AuthenticatorType; /** Base test class with common constants, data structures and methods */ public class AbstractDriverIT { @@ -326,6 +325,8 @@ public static Connection getConnection( properties.put("internal", Boolean.TRUE.toString()); // TODO: do we need this? properties.put("insecureMode", false); // use OCSP for all tests. properties.put("authenticator", AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name()); + properties.put("clientId", "IJF3fOhk3Ap614HGkKFt9+Ow1LA="); + properties.put("clientSecret", "kX0l9bGGLnuLkByufjUeSG0OLoi2Hz/Nw/31pKXqpE4="); if (injectSocketTimeout > 0) { properties.put("injectSocketTimeout", String.valueOf(injectSocketTimeout)); diff --git a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java index 08069b95c..8089387a2 100644 --- a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java +++ b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java @@ -25,6 +25,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; @@ -232,7 +233,8 @@ protected void addMapping(String mapping) { HttpPost postRequest = createWiremockPostRequest(mapping, "/__admin/mappings"); try (CloseableHttpClient client = HttpClients.createDefault(); CloseableHttpResponse response = client.execute(postRequest)) { - assertEquals(201, response.getStatusLine().getStatusCode()); + String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); + assertEquals(201, response.getStatusLine().getStatusCode(), responseBody); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java new file mode 100644 index 000000000..cbcb8649f --- /dev/null +++ b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java @@ -0,0 +1,136 @@ +package net.snowflake.client.jdbc; + +import static net.snowflake.client.core.SessionUtilExternalBrowser.*; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import net.snowflake.client.category.TestTags; +import net.snowflake.client.core.HttpClientSettingsKey; +import net.snowflake.client.core.OCSPMode; +import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SFLoginInput; +import net.snowflake.client.core.auth.oauth.AuthorizationCodeFlowAccessTokenProvider; +import net.snowflake.client.core.auth.oauth.OauthAccessTokenProvider; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Tag(TestTags.CORE) +public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + + public static final String SUCCESSFUL_FLOW_SCENARIO_MAPPINGS = + "{\n" + + " \"mappings\": [\n" + + " {\n" + + " \"scenarioName\": \"Successful OAuth authorization code flow\",\n" + + " \"requiredScenarioState\": \"Started\",\n" + + " \"newScenarioState\": \"Authorized\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" + + " \"method\": \"GET\"\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200\n" + + " },\n" + + " \"serveEventListeners\": [\n" + + " {\n" + + " \"name\": \"webhook\",\n" + + " \"parameters\": {\n" + + " \"method\": \"GET\",\n" + + " \"url\": \"http://localhost:8001/snowflake/oauth-redirect?code=123\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"scenarioName\": \"Successful OAuth authorization code flow\",\n" + + " \"requiredScenarioState\": \"Authorized\",\n" + + " \"newScenarioState\": \"Acquired access token\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/token-request.*\",\n" + + " \"method\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Authorization\": {\n" + + " \"contains\": \"Basic\"\n" + + " },\n" + + " \"Content-Type\": {\n" + + " \"contains\": \"application/x-www-form-urlencoded; charset=UTF-8\"\n" + + " }\n" + + " },\n" + + " \"bodyPatterns\": [{\n" + + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" + + " }]\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200,\n" + + " \"body\": \"{ \\\"access_token\\\" : \\\"access-token-123\\\", \\\"refresh_token\\\" : \\\"123\\\", \\\"token_type\\\" : \\\"Bearer\\\", \\\"username\\\" : \\\"user\\\", \\\"scope\\\" : \\\"refresh_token session:role:ANALYST\\\", \\\"expires_in\\\" : 600, \\\"refresh_token_expires_in\\\" : 86399, \\\"idpInitiated\\\" : false }\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"importOptions\": {\n" + + " \"duplicatePolicy\": \"IGNORE\",\n" + + " \"deleteAllNotInImport\": true\n" + + " }\n" + + "}"; + + private static final Logger log = + LoggerFactory.getLogger(OauthAuthorizationCodeFlowLatestIT.class); + + AuthExternalBrowserHandlers wiremockProxyRequestBrowserHandler = + new WiremockProxyRequestBrowserHandler(); + + @Test + public void successfulFlowScenario() throws SFException { + importMapping(SUCCESSFUL_FLOW_SCENARIO_MAPPINGS); + SFLoginInput loginInput = createLoginInputStub(); + + OauthAccessTokenProvider provider = + new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler); + String accessToken = provider.getAccessToken(loginInput); + + Assertions.assertTrue(StringUtils.isNotBlank(accessToken)); + Assertions.assertEquals("access-token-123", accessToken); + } + + private SFLoginInput createLoginInputStub() { + SFLoginInput loginInputStub = new SFLoginInput(); + loginInputStub.setServerUrl(String.format("http://%s:%d/", WIREMOCK_HOST, wiremockHttpPort)); + loginInputStub.setClientSecret("123"); + loginInputStub.setClientId("123"); + loginInputStub.setRole("ANALYST"); + loginInputStub.setSocketTimeout(Duration.ofMinutes(5)); + loginInputStub.setHttpClientSettingsKey(new HttpClientSettingsKey(OCSPMode.FAIL_OPEN)); + + return loginInputStub; + } + + static class WiremockProxyRequestBrowserHandler implements AuthExternalBrowserHandlers { + @Override + public HttpPost build(URI uri) { + // do nothing + return null; + } + + @Override + public void openBrowser(String ssoUrl) { + try (CloseableHttpClient client = HttpClients.createDefault()) { + client.execute(new HttpGet(ssoUrl)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void output(String msg) { + // do nothing + } + } +} From 4acd8df5d2721447a9bd0a4edaa326fcd09613a4 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Tue, 3 Dec 2024 07:51:11 +0100 Subject: [PATCH 05/39] refactored --- src/test/java/net/snowflake/client/AbstractDriverIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/net/snowflake/client/AbstractDriverIT.java b/src/test/java/net/snowflake/client/AbstractDriverIT.java index 113fedeee..db2c21901 100644 --- a/src/test/java/net/snowflake/client/AbstractDriverIT.java +++ b/src/test/java/net/snowflake/client/AbstractDriverIT.java @@ -325,8 +325,6 @@ public static Connection getConnection( properties.put("internal", Boolean.TRUE.toString()); // TODO: do we need this? properties.put("insecureMode", false); // use OCSP for all tests. properties.put("authenticator", AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name()); - properties.put("clientId", "IJF3fOhk3Ap614HGkKFt9+Ow1LA="); - properties.put("clientSecret", "kX0l9bGGLnuLkByufjUeSG0OLoi2Hz/Nw/31pKXqpE4="); if (injectSocketTimeout > 0) { properties.put("injectSocketTimeout", String.valueOf(injectSocketTimeout)); From 214c98cce9fc0441e75fbd1fcb08a80fa5d2ffeb Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Tue, 3 Dec 2024 14:00:18 +0100 Subject: [PATCH 06/39] Add test scenarios --- parent-pom.xml | 6 +- .../snowflake/client/core/SessionUtil.java | 5 +- ...horizationCodeFlowAccessTokenProvider.java | 24 ++-- .../OauthAuthorizationCodeFlowLatestIT.java | 109 +++++++++++++++++- thin_public_pom.xml | 4 +- 5 files changed, 130 insertions(+), 18 deletions(-) diff --git a/parent-pom.xml b/parent-pom.xml index 40a3656dd..b5093fd97 100644 --- a/parent-pom.xml +++ b/parent-pom.xml @@ -21,7 +21,7 @@ 4.4.16 1.5.6-5 17.0.0 - 9.3 + 9.6 1.8.1 4.2.0 1.12.655 @@ -60,7 +60,7 @@ 3.1.0 5.13.0 2.8.1 - 2.4.9 + 2.5.1 4.13.2 5.11.1 1.11.1 @@ -69,7 +69,7 @@ 2.2.0 4.11.0 4.1.115.Final - 9.37.3 + 9.40 11.20.1 0.31.1 1.0-alpha-9-stable-1 diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index 3a52b02e4..d9e58e9ab 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -140,6 +140,8 @@ public class SessionUtil { public static int DEFAULT_CLIENT_PREFETCH_THREADS = 4; public static int MIN_CLIENT_CHUNK_SIZE = 48; public static int MAX_CLIENT_CHUNK_SIZE = 160; + private static final int DEFAULT_BROWSER_AUTHORIZATION_TIMEOUT_SECONDS = 180; + public static Map JVM_PARAMS_TO_PARAMS = Stream.of( new String[][] { @@ -281,7 +283,8 @@ static SFLoginOutput openSession( "passing clientSecret is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); OauthAccessTokenProvider accessTokenProvider = new AuthorizationCodeFlowAccessTokenProvider( - new SessionUtilExternalBrowser.DefaultAuthExternalBrowserHandlers()); + new SessionUtilExternalBrowser.DefaultAuthExternalBrowserHandlers(), + DEFAULT_BROWSER_AUTHORIZATION_TIMEOUT_SECONDS); String oauthAccessToken = accessTokenProvider.getAccessToken(loginInput); loginInput.setAuthenticator(AuthenticatorType.OAUTH.name()); loginInput.setToken(oauthAccessToken); diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index 32e27e844..bcbb3ac7f 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -1,6 +1,6 @@ package net.snowflake.client.core.auth.oauth; -import static net.snowflake.client.core.SessionUtilExternalBrowser.*; +import static net.snowflake.client.core.SessionUtilExternalBrowser.AuthExternalBrowserHandlers; import com.amazonaws.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -52,13 +52,14 @@ public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessToke private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; public static final String SESSION_ROLE_SCOPE = "session:role"; - public static int AUTHORIZE_REDIRECT_TIMEOUT_MINUTES = 2; - private final AuthExternalBrowserHandlers browserHandler; private final ObjectMapper objectMapper = new ObjectMapper(); + private final int browserAuthorizationTimeoutSeconds; - public AuthorizationCodeFlowAccessTokenProvider(AuthExternalBrowserHandlers browserHandler) { + public AuthorizationCodeFlowAccessTokenProvider( + AuthExternalBrowserHandlers browserHandler, int browserAuthorizationTimeoutSeconds) { this.browserHandler = browserHandler; + this.browserAuthorizationTimeoutSeconds = browserAuthorizationTimeoutSeconds; } @Override @@ -75,21 +76,25 @@ private AuthorizationCode requestAuthorizationCode( URI authorizeRequestURI = request.toURI(); CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(loginInput.getRedirectUriPort()); + logger.debug( + "Waiting for authorization code on " + + buildRedirectURI(loginInput.getRedirectUriPort()) + + "..."); letUserAuthorizeViaBrowser(authorizeRequestURI); - String code = codeFuture.get(AUTHORIZE_REDIRECT_TIMEOUT_MINUTES, TimeUnit.MINUTES); + String code = codeFuture.get(this.browserAuthorizationTimeoutSeconds, TimeUnit.SECONDS); return new AuthorizationCode(code); } catch (Exception e) { if (e instanceof TimeoutException) { - logger.error( - "Authorization request timed out. Did not receive authorization code back to the redirect URI"); + throw new RuntimeException( + "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.", + e); } throw new RuntimeException(e); } } private String exchangeAuthorizationCodeForAccessToken( - SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) - throws SFException { + SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) { try { TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); String tokenResponse = @@ -126,6 +131,7 @@ private static CompletableFuture setupRedirectURIServerForAuthorizationC String authorizationCode = extractAuthorizationCodeFromQueryParameters(exchange.getRequestURI().getQuery()); if (!StringUtils.isNullOrEmpty(authorizationCode)) { + logger.debug("Received authorization code on redirect URI"); accessTokenFuture.complete(authorizationCode); httpServer.stop(0); } diff --git a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java index cbcb8649f..d085e0d7f 100644 --- a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java @@ -1,6 +1,6 @@ package net.snowflake.client.jdbc; -import static net.snowflake.client.core.SessionUtilExternalBrowser.*; +import static net.snowflake.client.core.SessionUtilExternalBrowser.AuthExternalBrowserHandlers; import java.io.IOException; import java.net.URI; @@ -26,7 +26,7 @@ @Tag(TestTags.CORE) public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { - public static final String SUCCESSFUL_FLOW_SCENARIO_MAPPINGS = + private static final String SUCCESSFUL_FLOW_SCENARIO_MAPPINGS = "{\n" + " \"mappings\": [\n" + " {\n" @@ -81,6 +81,81 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " }\n" + "}"; + public static final String BROWSER_TIMEOUT_SCENARIO_MAPPING = + "{\n" + + " \"mappings\": [\n" + + " {\n" + + " \"scenarioName\": \"Browser Authorization timeout\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" + + " \"method\": \"GET\"\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200,\n" + + " \"fixedDelayMilliseconds\": 5000\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"importOptions\": {\n" + + " \"duplicatePolicy\": \"IGNORE\",\n" + + " \"deleteAllNotInImport\": true\n" + + " }\n" + + "}"; + + public static final String TOKEN_REQUEST_ERROR_SCENARIO_MAPPING = + "{\n" + + " \"mappings\": [\n" + + " {\n" + + " \"scenarioName\": \"OAuth token request error\",\n" + + " \"requiredScenarioState\": \"Started\",\n" + + " \"newScenarioState\": \"Authorized\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" + + " \"method\": \"GET\"\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200\n" + + " },\n" + + " \"serveEventListeners\": [\n" + + " {\n" + + " \"name\": \"webhook\",\n" + + " \"parameters\": {\n" + + " \"method\": \"GET\",\n" + + " \"url\": \"http://localhost:8001/snowflake/oauth-redirect?code=123\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"scenarioName\": \"OAuth token request error\",\n" + + " \"requiredScenarioState\": \"Authorized\",\n" + + " \"newScenarioState\": \"Token request error\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/token-request.*\",\n" + + " \"method\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Authorization\": {\n" + + " \"contains\": \"Basic\"\n" + + " },\n" + + " \"Content-Type\": {\n" + + " \"contains\": \"application/x-www-form-urlencoded; charset=UTF-8\"\n" + + " }\n" + + " },\n" + + " \"bodyPatterns\": [{\n" + + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" + + " }]\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 400\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"importOptions\": {\n" + + " \"duplicatePolicy\": \"IGNORE\",\n" + + " \"deleteAllNotInImport\": true\n" + + " }\n" + + "}"; + private static final Logger log = LoggerFactory.getLogger(OauthAuthorizationCodeFlowLatestIT.class); @@ -93,13 +168,41 @@ public void successfulFlowScenario() throws SFException { SFLoginInput loginInput = createLoginInputStub(); OauthAccessTokenProvider provider = - new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler); + new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); String accessToken = provider.getAccessToken(loginInput); Assertions.assertTrue(StringUtils.isNotBlank(accessToken)); Assertions.assertEquals("access-token-123", accessToken); } + @Test + public void browserTimeoutFlowScenario() { + importMapping(BROWSER_TIMEOUT_SCENARIO_MAPPING); + SFLoginInput loginInput = createLoginInputStub(); + + OauthAccessTokenProvider provider = + new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 1); + RuntimeException e = + Assertions.assertThrows(RuntimeException.class, () -> provider.getAccessToken(loginInput)); + Assertions.assertEquals( + "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.", + e.getMessage()); + } + + @Test + public void tokenRequestErrorFlowScenario() { + importMapping(TOKEN_REQUEST_ERROR_SCENARIO_MAPPING); + SFLoginInput loginInput = createLoginInputStub(); + + OauthAccessTokenProvider provider = + new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); + RuntimeException e = + Assertions.assertThrows(RuntimeException.class, () -> provider.getAccessToken(loginInput)); + Assertions.assertEquals( + "net.snowflake.client.jdbc.SnowflakeSQLException: JDBC driver encountered communication error. Message: HTTP status=400.", + e.getMessage()); + } + private SFLoginInput createLoginInputStub() { SFLoginInput loginInputStub = new SFLoginInput(); loginInputStub.setServerUrl(String.format("http://%s:%d/", WIREMOCK_HOST, wiremockHttpPort)); diff --git a/thin_public_pom.xml b/thin_public_pom.xml index eeb42d0f0..b8527cade 100644 --- a/thin_public_pom.xml +++ b/thin_public_pom.xml @@ -55,11 +55,11 @@ 3.1.0 5.13.0 2.8.1 - 2.4.9 + 2.5.1 1.15.3 2.2.0 4.1.115.Final - 9.37.3 + 9.40.0 UTF-8 UTF-8 2.0.13 From dc559372ce5b640993e4845921973237969d9739 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 15:38:50 +0100 Subject: [PATCH 07/39] Added support for Okta oauth authorization code flow --- .../snowflake/client/core/SFLoginInput.java | 40 ++++- .../net/snowflake/client/core/SFSession.java | 11 +- .../client/core/SFSessionProperty.java | 5 +- .../snowflake/client/core/SessionUtil.java | 6 +- .../client/core/auth/AuthenticatorType.java | 2 +- ...horizationCodeFlowAccessTokenProvider.java | 165 ++++++++++-------- .../snowflake/client/AbstractDriverIT.java | 2 - .../OauthAuthorizationCodeFlowLatestIT.java | 85 +++++++-- 8 files changed, 213 insertions(+), 103 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/SFLoginInput.java b/src/main/java/net/snowflake/client/core/SFLoginInput.java index 0b9fe94cd..bf8f21ab0 100644 --- a/src/main/java/net/snowflake/client/core/SFLoginInput.java +++ b/src/main/java/net/snowflake/client/core/SFLoginInput.java @@ -55,9 +55,12 @@ public class SFLoginInput { private boolean enableClientRequestMfaToken; // OAuth - private int redirectUriPort = -1; + private String redirectUri; private String clientId; private String clientSecret; + private String externalAuthorizationUrl; + private String externalTokenRequestUrl; + private String scope; private Duration browserResponseTimeout; @@ -422,12 +425,12 @@ SFLoginInput setDisableSamlURLCheck(boolean disableSamlURLCheck) { return this; } - public int getRedirectUriPort() { - return redirectUriPort; + public String getRedirectUri() { + return redirectUri; } - public SFLoginInput setRedirectUriPort(int redirectUriPort) { - this.redirectUriPort = redirectUriPort; + public SFLoginInput setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; return this; } @@ -449,6 +452,33 @@ public SFLoginInput setClientSecret(String clientSecret) { return this; } + public String getExternalAuthorizationUrl() { + return externalAuthorizationUrl; + } + + public SFLoginInput setExternalAuthorizationUrl(String externalAuthorizationUrl) { + this.externalAuthorizationUrl = externalAuthorizationUrl; + return this; + } + + public String getExternalTokenRequestUrl() { + return externalTokenRequestUrl; + } + + public SFLoginInput setExternalTokenRequestUrl(String externalTokenRequestUrl) { + this.externalTokenRequestUrl = externalTokenRequestUrl; + return this; + } + + public String getScope() { + return scope; + } + + public SFLoginInput setScope(String scope) { + this.scope = scope; + return this; + } + Map getAdditionalHttpHeadersForSnowsight() { return additionalHttpHeadersForSnowsight; } diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index a9b969772..bf1c85b83 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -673,6 +673,12 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setPrivateKeyFile((String) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY_FILE)) .setClientId((String) connectionPropertiesMap.get(SFSessionProperty.CLIENT_ID)) .setClientSecret((String) connectionPropertiesMap.get(SFSessionProperty.CLIENT_SECRET)) + .setRedirectUri((String) connectionPropertiesMap.get(SFSessionProperty.OAUTH_REDIRECT_URI)) + .setScope((String) connectionPropertiesMap.get(SFSessionProperty.OAUTH_SCOPE)) + .setExternalAuthorizationUrl( + (String) connectionPropertiesMap.get(SFSessionProperty.EXTERNAL_AUTHORIZATION_URL)) + .setExternalTokenRequestUrl( + (String) connectionPropertiesMap.get(SFSessionProperty.EXTERNAL_TOKEN_REQUEST_URL)) .setPrivateKeyBase64( (String) connectionPropertiesMap.get(SFSessionProperty.PRIVATE_KEY_BASE64)) .setPrivateKeyPwd( @@ -698,11 +704,6 @@ public synchronized void open() throws SFException, SnowflakeSQLException { .setEnableClientRequestMfaToken(enableClientRequestMfaToken) .setBrowserResponseTimeout(browserResponseTimeout); - if (connectionPropertiesMap.containsKey(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)) { - loginInput.setRedirectUriPort( - (Integer) connectionPropertiesMap.get(SFSessionProperty.OAUTH_REDIRECT_URI_PORT)); - } - logger.info( "Connecting to {} Snowflake domain", loginInput.getHostFromServerUrl().toLowerCase().endsWith(".cn") ? "CHINA" : "GLOBAL"); diff --git a/src/main/java/net/snowflake/client/core/SFSessionProperty.java b/src/main/java/net/snowflake/client/core/SFSessionProperty.java index db9c386e7..f05bff338 100644 --- a/src/main/java/net/snowflake/client/core/SFSessionProperty.java +++ b/src/main/java/net/snowflake/client/core/SFSessionProperty.java @@ -29,9 +29,12 @@ public enum SFSessionProperty { AUTHENTICATOR("authenticator", false, String.class), OKTA_USERNAME("oktausername", false, String.class), PRIVATE_KEY("privateKey", false, PrivateKey.class), - OAUTH_REDIRECT_URI_PORT("oauthRedirectUriPort", false, Integer.class), + OAUTH_REDIRECT_URI("redirectUri", false, String.class), CLIENT_ID("clientID", false, String.class), CLIENT_SECRET("clientSecret", false, String.class), + OAUTH_SCOPE("scope", false, String.class), + EXTERNAL_AUTHORIZATION_URL("externalAuthorizationUrl", false, String.class), + EXTERNAL_TOKEN_REQUEST_URL("externalTokenRequestUrl", false, String.class), WAREHOUSE("warehouse", false, String.class), LOGIN_TIMEOUT("loginTimeout", false, Integer.class), NETWORK_TIMEOUT("networkTimeout", false, Integer.class), diff --git a/src/main/java/net/snowflake/client/core/SessionUtil.java b/src/main/java/net/snowflake/client/core/SessionUtil.java index d9e58e9ab..684e0d9b4 100644 --- a/src/main/java/net/snowflake/client/core/SessionUtil.java +++ b/src/main/java/net/snowflake/client/core/SessionUtil.java @@ -223,9 +223,9 @@ private static AuthenticatorType getAuthenticator(SFLoginInput loginInput) { return AuthenticatorType.EXTERNALBROWSER; } else if (loginInput .getAuthenticator() - .equalsIgnoreCase(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name())) { + .equalsIgnoreCase(AuthenticatorType.OAUTH_AUTHORIZATION_CODE.name())) { // OAuth authorization code flow authentication - return AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW; + return AuthenticatorType.OAUTH_AUTHORIZATION_CODE; } else if (loginInput.getAuthenticator().equalsIgnoreCase(AuthenticatorType.OAUTH.name())) { // OAuth access code Authentication return AuthenticatorType.OAUTH; @@ -274,7 +274,7 @@ static SFLoginOutput openSession( AssertUtil.assertTrue( loginInput.getLoginTimeout() >= 0, "negative login timeout for opening session"); - if (getAuthenticator(loginInput).equals(AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW)) { + if (getAuthenticator(loginInput).equals(AuthenticatorType.OAUTH_AUTHORIZATION_CODE)) { AssertUtil.assertTrue( loginInput.getClientId() != null, "passing clientId is required for OAUTH_AUTHORIZATION_CODE_FLOW authentication"); diff --git a/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java b/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java index 32d28dd83..a55e91370 100644 --- a/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java +++ b/src/main/java/net/snowflake/client/core/auth/AuthenticatorType.java @@ -46,5 +46,5 @@ public enum AuthenticatorType { /* * Authorization code flow with browser popup */ - OAUTH_AUTHORIZATION_CODE_FLOW + OAUTH_AUTHORIZATION_CODE } diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index bcbb3ac7f..df2fc4780 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -23,19 +23,22 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import net.snowflake.client.core.HttpUtil; import net.snowflake.client.core.SFException; import net.snowflake.client.core.SFLoginInput; import net.snowflake.client.core.SnowflakeJdbcInternalApi; 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 @@ -47,10 +50,10 @@ public class AuthorizationCodeFlowAccessTokenProvider implements OauthAccessToke private static final String SNOWFLAKE_AUTHORIZE_ENDPOINT = "/oauth/authorize"; private static final String SNOWFLAKE_TOKEN_REQUEST_ENDPOINT = "/oauth/token-request"; - private static final String REDIRECT_URI_HOST = "localhost"; - private static final int DEFAULT_REDIRECT_URI_PORT = 8001; + private static final String DEFAULT_REDIRECT_HOST = "http://localhost:8001"; private static final String REDIRECT_URI_ENDPOINT = "/snowflake/oauth-redirect"; - public static final String SESSION_ROLE_SCOPE = "session:role"; + 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(); @@ -64,42 +67,32 @@ public AuthorizationCodeFlowAccessTokenProvider( @Override public String getAccessToken(SFLoginInput loginInput) throws SFException { - CodeVerifier pkceVerifier = new CodeVerifier(); - AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput, pkceVerifier); - return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode, pkceVerifier); - } - - private AuthorizationCode requestAuthorizationCode( - SFLoginInput loginInput, CodeVerifier pkceVerifier) throws SFException { try { - AuthorizationRequest request = buildAuthorizationRequest(loginInput, pkceVerifier); - URI authorizeRequestURI = request.toURI(); - CompletableFuture codeFuture = - setupRedirectURIServerForAuthorizationCode(loginInput.getRedirectUriPort()); - logger.debug( - "Waiting for authorization code on " - + buildRedirectURI(loginInput.getRedirectUriPort()) - + "..."); - letUserAuthorizeViaBrowser(authorizeRequestURI); - String code = codeFuture.get(this.browserAuthorizationTimeoutSeconds, TimeUnit.SECONDS); - return new AuthorizationCode(code); + CodeVerifier pkceVerifier = new CodeVerifier(); + AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput, pkceVerifier); + return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode, pkceVerifier); } catch (Exception e) { - if (e instanceof TimeoutException) { - throw new RuntimeException( - "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.", - e); - } throw new RuntimeException(e); } } + private AuthorizationCode requestAuthorizationCode( + SFLoginInput loginInput, CodeVerifier pkceVerifier) throws SFException, IOException { + AuthorizationRequest request = buildAuthorizationRequest(loginInput, pkceVerifier); + URI authorizeRequestURI = request.toURI(); + HttpServer httpServer = createHttpServer(loginInput); + CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(httpServer); + logger.debug("Waiting for authorization code on " + buildRedirectUri(loginInput) + "..."); + return letUserAuthorize(authorizeRequestURI, codeFuture, httpServer); + } + private String exchangeAuthorizationCodeForAccessToken( SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) { try { TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); String tokenResponse = HttpUtil.executeGeneralRequest( - convertTokenRequest(request.toHTTPRequest()), + convertToBaseRequest(request.toHTTPRequest()), loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeoutInMillis(), @@ -113,85 +106,117 @@ private String exchangeAuthorizationCodeForAccessToken( } } - private void letUserAuthorizeViaBrowser(URI authorizeRequestURI) throws SFException { - browserHandler.openBrowser(authorizeRequestURI.toString()); + private AuthorizationCode letUserAuthorize( + URI authorizeRequestURI, CompletableFuture codeFuture, HttpServer httpServer) + throws SFException { + try { + browserHandler.openBrowser(authorizeRequestURI.toString()); + String code = codeFuture.get(this.browserAuthorizationTimeoutSeconds, TimeUnit.SECONDS); + return new AuthorizationCode(code); + } catch (Exception e) { + httpServer.stop(0); + if (e instanceof TimeoutException) { + throw new RuntimeException( + "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.", + e); + } + throw new RuntimeException(e); + } } private static CompletableFuture setupRedirectURIServerForAuthorizationCode( - int redirectUriPort) throws IOException { + HttpServer httpServer) { CompletableFuture accessTokenFuture = new CompletableFuture<>(); - int redirectPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; - HttpServer httpServer = - HttpServer.create(new InetSocketAddress(REDIRECT_URI_HOST, redirectPort), 0); httpServer.createContext( REDIRECT_URI_ENDPOINT, exchange -> { exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().close(); - String authorizationCode = - extractAuthorizationCodeFromQueryParameters(exchange.getRequestURI().getQuery()); - if (!StringUtils.isNullOrEmpty(authorizationCode)) { - logger.debug("Received authorization code on redirect URI"); - accessTokenFuture.complete(authorizationCode); - httpServer.stop(0); + Map urlParams = + URLEncodedUtils.parse(exchange.getRequestURI(), StandardCharsets.UTF_8).stream() + .collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); + if (urlParams.containsKey("error")) { + accessTokenFuture.completeExceptionally( + new RuntimeException( + String.format( + "Error during authorization: %s, %s", + urlParams.get("error"), urlParams.get("error_description")))); + } else { + String authorizationCode = urlParams.get("code"); + if (!StringUtils.isNullOrEmpty(authorizationCode)) { + logger.debug("Received authorization code on redirect URI"); + accessTokenFuture.complete(authorizationCode); + httpServer.stop(0); + } } }); httpServer.start(); return accessTokenFuture; } + private static HttpServer createHttpServer(SFLoginInput loginInput) throws IOException { + URI redirectUri = buildRedirectUri(loginInput); + return HttpServer.create( + new InetSocketAddress(redirectUri.getHost(), redirectUri.getPort()), 0); + } + private static AuthorizationRequest buildAuthorizationRequest( - SFLoginInput loginInput, CodeVerifier pkceVerifier) throws URISyntaxException { - URI authorizeEndpoint = new URI(loginInput.getServerUrl() + SNOWFLAKE_AUTHORIZE_ENDPOINT); + SFLoginInput loginInput, CodeVerifier pkceVerifier) { ClientID clientID = new ClientID(loginInput.getClientId()); - Scope scope = new Scope(String.format("%s:%s", SESSION_ROLE_SCOPE, loginInput.getRole())); - URI callback = buildRedirectURI(loginInput.getRedirectUriPort()); + URI callback = buildRedirectUri(loginInput); State state = new State(256); + String scope = getScope(loginInput); return new AuthorizationRequest.Builder(new ResponseType(ResponseType.Value.CODE), clientID) - .scope(scope) + .scope(new Scope(scope)) .state(state) .redirectionURI(callback) .codeChallenge(pkceVerifier, CodeChallengeMethod.S256) - .endpointURI(authorizeEndpoint) + .endpointURI(getAuthorizationUrl(loginInput)) .build(); } private static TokenRequest buildTokenRequest( - SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) - throws URISyntaxException { - URI redirectURI = buildRedirectURI(loginInput.getRedirectUriPort()); + SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) { + URI redirectUri = buildRedirectUri(loginInput); AuthorizationGrant codeGrant = - new AuthorizationCodeGrant(authorizationCode, redirectURI, pkceVerifier); + new AuthorizationCodeGrant(authorizationCode, redirectUri, pkceVerifier); ClientAuthentication clientAuthentication = new ClientSecretBasic( new ClientID(loginInput.getClientId()), new Secret(loginInput.getClientSecret())); - URI tokenEndpoint = - new URI(String.format(loginInput.getServerUrl() + SNOWFLAKE_TOKEN_REQUEST_ENDPOINT)); - Scope scope = new Scope(SESSION_ROLE_SCOPE, loginInput.getRole()); - return new TokenRequest(tokenEndpoint, clientAuthentication, codeGrant, scope); + Scope scope = new Scope(getScope(loginInput)); + return new TokenRequest(getTokenRequestUrl(loginInput), clientAuthentication, codeGrant, scope); } - private static URI buildRedirectURI(int redirectUriPort) throws URISyntaxException { - redirectUriPort = (redirectUriPort != -1) ? redirectUriPort : DEFAULT_REDIRECT_URI_PORT; - return new URI( - String.format("http://%s:%s%s", REDIRECT_URI_HOST, redirectUriPort, REDIRECT_URI_ENDPOINT)); + private static URI buildRedirectUri(SFLoginInput loginInput) { + String redirectUri = + !StringUtils.isNullOrEmpty(loginInput.getRedirectUri()) + ? loginInput.getRedirectUri() + : DEFAULT_REDIRECT_URI; + return URI.create(redirectUri); } - private static String extractAuthorizationCodeFromQueryParameters(String queryParameters) { - String prefix = "code="; - String codeSuffix = - queryParameters.substring(queryParameters.indexOf(prefix) + prefix.length()); - if (codeSuffix.contains("&")) { - return codeSuffix.substring(0, codeSuffix.indexOf("&")); - } else { - return codeSuffix; - } - } - - private static HttpRequestBase convertTokenRequest(HTTPRequest nimbusRequest) { + private static HttpRequestBase convertToBaseRequest(HTTPRequest nimbusRequest) { HttpPost request = new HttpPost(nimbusRequest.getURI()); request.setEntity(new StringEntity(nimbusRequest.getBody(), StandardCharsets.UTF_8)); nimbusRequest.getHeaderMap().forEach((key, values) -> request.addHeader(key, values.get(0))); return request; } + + private static URI getAuthorizationUrl(SFLoginInput loginInput) { + return !StringUtils.isNullOrEmpty(loginInput.getExternalAuthorizationUrl()) + ? URI.create(loginInput.getExternalAuthorizationUrl()) + : URI.create(loginInput.getServerUrl() + SNOWFLAKE_AUTHORIZE_ENDPOINT); + } + + private static URI getTokenRequestUrl(SFLoginInput loginInput) { + return !StringUtils.isNullOrEmpty(loginInput.getExternalTokenRequestUrl()) + ? URI.create(loginInput.getExternalTokenRequestUrl()) + : URI.create(loginInput.getServerUrl() + SNOWFLAKE_TOKEN_REQUEST_ENDPOINT); + } + + private static String getScope(SFLoginInput loginInput) { + return (!StringUtils.isNullOrEmpty(loginInput.getScope())) + ? loginInput.getScope() + : DEFAULT_SESSION_ROLE_SCOPE_PREFIX + loginInput.getRole(); + } } diff --git a/src/test/java/net/snowflake/client/AbstractDriverIT.java b/src/test/java/net/snowflake/client/AbstractDriverIT.java index db2c21901..3104ce7e9 100644 --- a/src/test/java/net/snowflake/client/AbstractDriverIT.java +++ b/src/test/java/net/snowflake/client/AbstractDriverIT.java @@ -24,7 +24,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; -import net.snowflake.client.core.auth.AuthenticatorType; /** Base test class with common constants, data structures and methods */ public class AbstractDriverIT { @@ -324,7 +323,6 @@ public static Connection getConnection( properties.put("internal", Boolean.TRUE.toString()); // TODO: do we need this? properties.put("insecureMode", false); // use OCSP for all tests. - properties.put("authenticator", AuthenticatorType.OAUTH_AUTHORIZATION_CODE_FLOW.name()); if (injectSocketTimeout > 0) { properties.put("injectSocketTimeout", String.valueOf(injectSocketTimeout)); diff --git a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java index d085e0d7f..0121f0754 100644 --- a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java @@ -12,6 +12,7 @@ import net.snowflake.client.core.SFLoginInput; import net.snowflake.client.core.auth.oauth.AuthorizationCodeFlowAccessTokenProvider; import net.snowflake.client.core.auth.oauth.OauthAccessTokenProvider; +import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; @@ -81,7 +82,7 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " }\n" + "}"; - public static final String BROWSER_TIMEOUT_SCENARIO_MAPPING = + private static final String BROWSER_TIMEOUT_SCENARIO_MAPPING = "{\n" + " \"mappings\": [\n" + " {\n" @@ -102,7 +103,36 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " }\n" + "}"; - public static final String TOKEN_REQUEST_ERROR_SCENARIO_MAPPING = + private static final String INVALID_SCOPE_SCENARIO_MAPPING = + "{\n" + + " \"mappings\": [\n" + + " {\n" + + " \"scenarioName\": \"Invalid scope authorization error\",\n" + + " \"request\": {\n" + + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" + + " \"method\": \"GET\"\n" + + " },\n" + + " \"response\": {\n" + + " \"status\": 200\n" + + " },\n" + + " \"serveEventListeners\": [\n" + + " {\n" + + " \"name\": \"webhook\",\n" + + " \"parameters\": {\n" + + " \"method\": \"GET\",\n" + + " \"url\": \"http://localhost:8002/snowflake/oauth-redirect?error=invalid_scope&error_description=One+or+more+scopes+are+not+configured+for+the+authorization+server+resource.\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"importOptions\": {\n" + + " \"duplicatePolicy\": \"IGNORE\",\n" + + " \"deleteAllNotInImport\": true\n" + + " }\n" + + "}"; + + private static final String TOKEN_REQUEST_ERROR_SCENARIO_MAPPING = "{\n" + " \"mappings\": [\n" + " {\n" @@ -121,7 +151,7 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " \"name\": \"webhook\",\n" + " \"parameters\": {\n" + " \"method\": \"GET\",\n" - + " \"url\": \"http://localhost:8001/snowflake/oauth-redirect?code=123\"\n" + + " \"url\": \"http://localhost:8003/snowflake/oauth-redirect?code=123\"\n" + " }\n" + " }\n" + " ]\n" @@ -142,7 +172,7 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " }\n" + " },\n" + " \"bodyPatterns\": [{\n" - + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" + + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8003%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" + " }]\n" + " },\n" + " \"response\": {\n" @@ -156,7 +186,7 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + " }\n" + "}"; - private static final Logger log = + private static final Logger logger = LoggerFactory.getLogger(OauthAuthorizationCodeFlowLatestIT.class); AuthExternalBrowserHandlers wiremockProxyRequestBrowserHandler = @@ -165,7 +195,8 @@ public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { @Test public void successfulFlowScenario() throws SFException { importMapping(SUCCESSFUL_FLOW_SCENARIO_MAPPINGS); - SFLoginInput loginInput = createLoginInputStub(); + SFLoginInput loginInput = + createLoginInputStub("http://localhost:8001/snowflake/oauth-redirect"); OauthAccessTokenProvider provider = new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); @@ -178,37 +209,58 @@ public void successfulFlowScenario() throws SFException { @Test public void browserTimeoutFlowScenario() { importMapping(BROWSER_TIMEOUT_SCENARIO_MAPPING); - SFLoginInput loginInput = createLoginInputStub(); + SFLoginInput loginInput = + createLoginInputStub("http://localhost:8004/snowflake/oauth-redirect"); OauthAccessTokenProvider provider = new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 1); RuntimeException e = Assertions.assertThrows(RuntimeException.class, () -> provider.getAccessToken(loginInput)); - Assertions.assertEquals( - "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.", - e.getMessage()); + Assertions.assertTrue( + e.getMessage() + .contains( + "Authorization request timed out. Snowflake driver did not receive authorization code back to the redirect URI. Verify your security integration and driver configuration.")); + } + + @Test + public void invalidScopeFlowScenario() { + importMapping(INVALID_SCOPE_SCENARIO_MAPPING); + SFLoginInput loginInput = + createLoginInputStub("http://localhost:8002/snowflake/oauth-redirect"); + + OauthAccessTokenProvider provider = + new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); + RuntimeException e = + Assertions.assertThrows(RuntimeException.class, () -> provider.getAccessToken(loginInput)); + Assertions.assertTrue( + e.getMessage() + .contains( + "Error during authorization: invalid_scope, One or more scopes are not configured for the authorization server resource.")); } @Test public void tokenRequestErrorFlowScenario() { importMapping(TOKEN_REQUEST_ERROR_SCENARIO_MAPPING); - SFLoginInput loginInput = createLoginInputStub(); + SFLoginInput loginInput = + createLoginInputStub("http://localhost:8003/snowflake/oauth-redirect"); OauthAccessTokenProvider provider = new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); RuntimeException e = Assertions.assertThrows(RuntimeException.class, () -> provider.getAccessToken(loginInput)); - Assertions.assertEquals( - "net.snowflake.client.jdbc.SnowflakeSQLException: JDBC driver encountered communication error. Message: HTTP status=400.", - e.getMessage()); + Assertions.assertTrue( + e.getMessage() + .contains( + "net.snowflake.client.jdbc.SnowflakeSQLException: JDBC driver encountered communication error. Message: HTTP status=400.")); } - private SFLoginInput createLoginInputStub() { + private SFLoginInput createLoginInputStub(String redirectUri) { SFLoginInput loginInputStub = new SFLoginInput(); loginInputStub.setServerUrl(String.format("http://%s:%d/", WIREMOCK_HOST, wiremockHttpPort)); loginInputStub.setClientSecret("123"); loginInputStub.setClientId("123"); loginInputStub.setRole("ANALYST"); + loginInputStub.setRedirectUri(redirectUri); loginInputStub.setSocketTimeout(Duration.ofMinutes(5)); loginInputStub.setHttpClientSettingsKey(new HttpClientSettingsKey(OCSPMode.FAIL_OPEN)); @@ -225,7 +277,8 @@ public HttpPost build(URI uri) { @Override public void openBrowser(String ssoUrl) { try (CloseableHttpClient client = HttpClients.createDefault()) { - client.execute(new HttpGet(ssoUrl)); + HttpResponse response = client.execute(new HttpGet(ssoUrl)); + logger.debug(response.toString()); } catch (IOException e) { throw new RuntimeException(e); } From 857d8a2bf2515c44da34293f5b634121f0b8177e Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 16:07:57 +0100 Subject: [PATCH 08/39] Add logs --- .../oauth/AuthorizationCodeFlowAccessTokenProvider.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index df2fc4780..5e763d882 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -72,6 +72,7 @@ public String getAccessToken(SFLoginInput loginInput) throws SFException { AuthorizationCode authorizationCode = requestAuthorizationCode(loginInput, pkceVerifier); return exchangeAuthorizationCodeForAccessToken(loginInput, authorizationCode, pkceVerifier); } catch (Exception e) { + logger.debug("Error during OAuth authorization code flow: " + e.getMessage()); throw new RuntimeException(e); } } @@ -80,9 +81,10 @@ private AuthorizationCode requestAuthorizationCode( SFLoginInput loginInput, CodeVerifier pkceVerifier) throws SFException, IOException { AuthorizationRequest request = buildAuthorizationRequest(loginInput, pkceVerifier); URI authorizeRequestURI = request.toURI(); + logger.debug("Executing authorization code request to: " + authorizeRequestURI.getAuthority() + authorizeRequestURI.getPath()); HttpServer httpServer = createHttpServer(loginInput); CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(httpServer); - logger.debug("Waiting for authorization code on " + buildRedirectUri(loginInput) + "..."); + logger.debug("Waiting for authorization code redirection to " + buildRedirectUri(loginInput) + "..."); return letUserAuthorize(authorizeRequestURI, codeFuture, httpServer); } @@ -90,6 +92,8 @@ private String exchangeAuthorizationCodeForAccessToken( SFLoginInput loginInput, AuthorizationCode authorizationCode, CodeVerifier pkceVerifier) { try { TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); + URI requestUri = request.getEndpointURI(); + logger.debug("Requesting access token from: " + requestUri.getAuthority() + requestUri.getPath()); String tokenResponse = HttpUtil.executeGeneralRequest( convertToBaseRequest(request.toHTTPRequest()), @@ -100,6 +104,7 @@ private String exchangeAuthorizationCodeForAccessToken( loginInput.getHttpClientSettingsKey()); TokenResponseDTO tokenResponseDTO = objectMapper.readValue(tokenResponse, TokenResponseDTO.class); + logger.debug("Received OAuth access token from: " + requestUri.getAuthority() + requestUri.getPath()); return tokenResponseDTO.getAccessToken(); } catch (Exception e) { throw new RuntimeException(e); @@ -130,8 +135,6 @@ private static CompletableFuture setupRedirectURIServerForAuthorizationC httpServer.createContext( REDIRECT_URI_ENDPOINT, exchange -> { - exchange.sendResponseHeaders(200, 0); - exchange.getResponseBody().close(); Map urlParams = URLEncodedUtils.parse(exchange.getRequestURI(), StandardCharsets.UTF_8).stream() .collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); From 4662dfce194411aa34a5add17c46c511c4c2b59d Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 16:12:24 +0100 Subject: [PATCH 09/39] format --- .../AuthorizationCodeFlowAccessTokenProvider.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java index 5e763d882..dc6be9700 100644 --- a/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java +++ b/src/main/java/net/snowflake/client/core/auth/oauth/AuthorizationCodeFlowAccessTokenProvider.java @@ -81,10 +81,14 @@ private AuthorizationCode requestAuthorizationCode( SFLoginInput loginInput, CodeVerifier pkceVerifier) throws SFException, IOException { AuthorizationRequest request = buildAuthorizationRequest(loginInput, pkceVerifier); URI authorizeRequestURI = request.toURI(); - logger.debug("Executing authorization code request to: " + authorizeRequestURI.getAuthority() + authorizeRequestURI.getPath()); + logger.debug( + "Executing authorization code request to: " + + authorizeRequestURI.getAuthority() + + authorizeRequestURI.getPath()); HttpServer httpServer = createHttpServer(loginInput); CompletableFuture codeFuture = setupRedirectURIServerForAuthorizationCode(httpServer); - logger.debug("Waiting for authorization code redirection to " + buildRedirectUri(loginInput) + "..."); + logger.debug( + "Waiting for authorization code redirection to " + buildRedirectUri(loginInput) + "..."); return letUserAuthorize(authorizeRequestURI, codeFuture, httpServer); } @@ -93,7 +97,8 @@ private String exchangeAuthorizationCodeForAccessToken( try { TokenRequest request = buildTokenRequest(loginInput, authorizationCode, pkceVerifier); URI requestUri = request.getEndpointURI(); - logger.debug("Requesting access token from: " + requestUri.getAuthority() + requestUri.getPath()); + logger.debug( + "Requesting access token from: " + requestUri.getAuthority() + requestUri.getPath()); String tokenResponse = HttpUtil.executeGeneralRequest( convertToBaseRequest(request.toHTTPRequest()), @@ -104,7 +109,8 @@ private String exchangeAuthorizationCodeForAccessToken( loginInput.getHttpClientSettingsKey()); TokenResponseDTO tokenResponseDTO = objectMapper.readValue(tokenResponse, TokenResponseDTO.class); - logger.debug("Received OAuth access token from: " + requestUri.getAuthority() + requestUri.getPath()); + logger.debug( + "Received OAuth access token from: " + requestUri.getAuthority() + requestUri.getPath()); return tokenResponseDTO.getAccessToken(); } catch (Exception e) { throw new RuntimeException(e); From 06b0e24701fed1570ceaad4dc6df400dd72c0c89 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 22:27:44 +0100 Subject: [PATCH 10/39] reformat --- src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java | 4 +--- .../client/jdbc/OauthAuthorizationCodeFlowLatestIT.java | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java index 8089387a2..08069b95c 100644 --- a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java +++ b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java @@ -25,7 +25,6 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; @@ -233,8 +232,7 @@ protected void addMapping(String mapping) { HttpPost postRequest = createWiremockPostRequest(mapping, "/__admin/mappings"); try (CloseableHttpClient client = HttpClients.createDefault(); CloseableHttpResponse response = client.execute(postRequest)) { - String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8"); - assertEquals(201, response.getStatusLine().getStatusCode(), responseBody); + assertEquals(201, response.getStatusLine().getStatusCode()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java index 0121f0754..2ed6edf5c 100644 --- a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java @@ -2,6 +2,7 @@ import static net.snowflake.client.core.SessionUtilExternalBrowser.AuthExternalBrowserHandlers; +import com.amazonaws.util.StringUtils; import java.io.IOException; import java.net.URI; import java.time.Duration; @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.platform.commons.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -202,7 +202,7 @@ public void successfulFlowScenario() throws SFException { new AuthorizationCodeFlowAccessTokenProvider(wiremockProxyRequestBrowserHandler, 30); String accessToken = provider.getAccessToken(loginInput); - Assertions.assertTrue(StringUtils.isNotBlank(accessToken)); + Assertions.assertFalse(StringUtils.isNullOrEmpty(accessToken)); Assertions.assertEquals("access-token-123", accessToken); } From 86d61ef17fe89f8c6a4135a5131e587c778d4388 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 22:56:45 +0100 Subject: [PATCH 11/39] Fix shading --- FIPS/scripts/check_content.sh | 2 +- ci/scripts/check_content.sh | 2 +- .../java/net/snowflake/client/core/auth/ClientAuthnDTO.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FIPS/scripts/check_content.sh b/FIPS/scripts/check_content.sh index a30eacec6..236114df6 100755 --- a/FIPS/scripts/check_content.sh +++ b/FIPS/scripts/check_content.sh @@ -6,7 +6,7 @@ set -o pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -if jar tvf $DIR/../target/snowflake-jdbc-fips.jar | awk '{print $8}' | grep -v -E "/$" | grep -v -E "^(net|com)/snowflake" | grep -v -E "(com|net)/\$" | grep -v -E "^META-INF" | grep -v -E "^mozilla" | grep -v -E "^com/sun/jna" | grep -v com/sun/ | grep -v mime.types | grep -v -E "^com/github/luben/zstd/" | grep -v -E "^aix/" | grep -v -E "^darwin/" | grep -v -E "^freebsd/" | grep -v -E "^linux/" | grep -v -E "^win/"; then +if jar tvf $DIR/../target/snowflake-jdbc-fips.jar | awk '{print $8}' | grep -v -E "/$" | grep -v -E "^(net|com)/snowflake" | grep -v -E "(com|net)/\$" | grep -v -E "^META-INF" | grep -v -E "^iso3166_" | grep -v -E "^mozilla" | grep -v -E "^com/sun/jna" | grep -v com/sun/ | grep -v mime.types | grep -v -E "^com/github/luben/zstd/" | grep -v -E "^aix/" | grep -v -E "^darwin/" | grep -v -E "^freebsd/" | grep -v -E "^linux/" | grep -v -E "^win/"; then echo "[ERROR] JDBC jar includes class not under the snowflake namespace" exit 1 fi diff --git a/ci/scripts/check_content.sh b/ci/scripts/check_content.sh index 1af33e56a..128648483 100755 --- a/ci/scripts/check_content.sh +++ b/ci/scripts/check_content.sh @@ -8,7 +8,7 @@ set -o pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -if jar tvf $DIR/../../target/snowflake-jdbc${package_modifier}.jar | awk '{print $8}' | grep -v -E "/$" | grep -v -E "^(net|com)/snowflake" | grep -v -E "(com|net)/\$" | grep -v -E "^META-INF" | grep -v -E "^mozilla" | grep -v -E "^com/sun/jna" | grep -v com/sun/ | grep -v mime.types | grep -v -E "^com/github/luben/zstd/" | grep -v -E "^aix/" | grep -v -E "^darwin/" | grep -v -E "^freebsd/" | grep -v -E "^linux/" | grep -v -E "^win/"; then +if jar tvf snowflake-jdbc.jar | awk '{print $8}' | grep -v -E "/$" | grep -v -E "^(net|com)/snowflake" | grep -v -E "(com|net)/\$" | grep -v -E "^META-INF" | grep -v -E "^iso3166_" | grep -v -E "^mozilla" | grep -v -E "^com/sun/jna" | grep -v com/sun/ | grep -v mime.types | grep -v -E "^com/github/luben/zstd/" | grep -v -E "^aix/" | grep -v -E "^darwin/" | grep -v -E "^freebsd/" | grep -v -E "^linux/" | grep -v -E "^win/"; then echo "[ERROR] JDBC jar includes class not under the snowflake namespace" exit 1 fi diff --git a/src/main/java/net/snowflake/client/core/auth/ClientAuthnDTO.java b/src/main/java/net/snowflake/client/core/auth/ClientAuthnDTO.java index 98626d86a..46e4d755f 100644 --- a/src/main/java/net/snowflake/client/core/auth/ClientAuthnDTO.java +++ b/src/main/java/net/snowflake/client/core/auth/ClientAuthnDTO.java @@ -24,12 +24,12 @@ public ClientAuthnDTO(Map data, @Nullable String inFlightCtx) { this.inFlightCtx = inFlightCtx; } - /** Required by Jackson */ + // Required by Jackson public Map getData() { return data; } - /** Required by Jackson */ + // Required by Jackson public String getInFlightCtx() { return inFlightCtx; } From c8de8844d1a8f4053a69912ad76fa97f5586bbb0 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Wed, 4 Dec 2024 23:46:55 +0100 Subject: [PATCH 12/39] Extracted wiremock config to files --- .../client/jdbc/BaseWiremockTest.java | 13 ++ .../OauthAuthorizationCodeFlowLatestIT.java | 169 ++---------------- .../browser_timeout_scenario_mapping.json | 16 ++ .../invalid_scope_scenario_mapping.json | 23 +++ .../successful_scenario_mapping.json | 51 ++++++ .../token_request_error_scenario_mapping.json | 50 ++++++ 6 files changed, 163 insertions(+), 159 deletions(-) create mode 100644 src/test/resources/oauth/authorization_code/browser_timeout_scenario_mapping.json create mode 100644 src/test/resources/oauth/authorization_code/invalid_scope_scenario_mapping.json create mode 100644 src/test/resources/oauth/authorization_code/successful_scenario_mapping.json create mode 100644 src/test/resources/oauth/authorization_code/token_request_error_scenario_mapping.json diff --git a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java index 08069b95c..031c1d33e 100644 --- a/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java +++ b/src/test/java/net/snowflake/client/jdbc/BaseWiremockTest.java @@ -10,15 +10,19 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.ServerSocket; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.Properties; import net.snowflake.client.core.HttpUtil; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; +import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; @@ -237,4 +241,13 @@ protected void addMapping(String mapping) { throw new RuntimeException(e); } } + + protected void importMappingFromResources(String relativePath) { + try (InputStream is = BaseWiremockTest.class.getResourceAsStream(relativePath)) { + String scenario = IOUtils.toString(Objects.requireNonNull(is), StandardCharsets.UTF_8); + importMapping(scenario); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java index 2ed6edf5c..687fc3930 100644 --- a/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/OauthAuthorizationCodeFlowLatestIT.java @@ -27,174 +27,25 @@ @Tag(TestTags.CORE) public class OauthAuthorizationCodeFlowLatestIT extends BaseWiremockTest { + private static final String SCENARIOS_BASE_DIR = "/oauth/authorization_code"; private static final String SUCCESSFUL_FLOW_SCENARIO_MAPPINGS = - "{\n" - + " \"mappings\": [\n" - + " {\n" - + " \"scenarioName\": \"Successful OAuth authorization code flow\",\n" - + " \"requiredScenarioState\": \"Started\",\n" - + " \"newScenarioState\": \"Authorized\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" - + " \"method\": \"GET\"\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 200\n" - + " },\n" - + " \"serveEventListeners\": [\n" - + " {\n" - + " \"name\": \"webhook\",\n" - + " \"parameters\": {\n" - + " \"method\": \"GET\",\n" - + " \"url\": \"http://localhost:8001/snowflake/oauth-redirect?code=123\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"scenarioName\": \"Successful OAuth authorization code flow\",\n" - + " \"requiredScenarioState\": \"Authorized\",\n" - + " \"newScenarioState\": \"Acquired access token\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/token-request.*\",\n" - + " \"method\": \"POST\",\n" - + " \"headers\": {\n" - + " \"Authorization\": {\n" - + " \"contains\": \"Basic\"\n" - + " },\n" - + " \"Content-Type\": {\n" - + " \"contains\": \"application/x-www-form-urlencoded; charset=UTF-8\"\n" - + " }\n" - + " },\n" - + " \"bodyPatterns\": [{\n" - + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" - + " }]\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 200,\n" - + " \"body\": \"{ \\\"access_token\\\" : \\\"access-token-123\\\", \\\"refresh_token\\\" : \\\"123\\\", \\\"token_type\\\" : \\\"Bearer\\\", \\\"username\\\" : \\\"user\\\", \\\"scope\\\" : \\\"refresh_token session:role:ANALYST\\\", \\\"expires_in\\\" : 600, \\\"refresh_token_expires_in\\\" : 86399, \\\"idpInitiated\\\" : false }\"\n" - + " }\n" - + " }\n" - + " ],\n" - + " \"importOptions\": {\n" - + " \"duplicatePolicy\": \"IGNORE\",\n" - + " \"deleteAllNotInImport\": true\n" - + " }\n" - + "}"; - + SCENARIOS_BASE_DIR + "/successful_scenario_mapping.json"; private static final String BROWSER_TIMEOUT_SCENARIO_MAPPING = - "{\n" - + " \"mappings\": [\n" - + " {\n" - + " \"scenarioName\": \"Browser Authorization timeout\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" - + " \"method\": \"GET\"\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 200,\n" - + " \"fixedDelayMilliseconds\": 5000\n" - + " }\n" - + " }\n" - + " ],\n" - + " \"importOptions\": {\n" - + " \"duplicatePolicy\": \"IGNORE\",\n" - + " \"deleteAllNotInImport\": true\n" - + " }\n" - + "}"; - + SCENARIOS_BASE_DIR + "/browser_timeout_scenario_mapping.json"; private static final String INVALID_SCOPE_SCENARIO_MAPPING = - "{\n" - + " \"mappings\": [\n" - + " {\n" - + " \"scenarioName\": \"Invalid scope authorization error\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" - + " \"method\": \"GET\"\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 200\n" - + " },\n" - + " \"serveEventListeners\": [\n" - + " {\n" - + " \"name\": \"webhook\",\n" - + " \"parameters\": {\n" - + " \"method\": \"GET\",\n" - + " \"url\": \"http://localhost:8002/snowflake/oauth-redirect?error=invalid_scope&error_description=One+or+more+scopes+are+not+configured+for+the+authorization+server+resource.\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ],\n" - + " \"importOptions\": {\n" - + " \"duplicatePolicy\": \"IGNORE\",\n" - + " \"deleteAllNotInImport\": true\n" - + " }\n" - + "}"; - + SCENARIOS_BASE_DIR + "/invalid_scope_scenario_mapping.json"; private static final String TOKEN_REQUEST_ERROR_SCENARIO_MAPPING = - "{\n" - + " \"mappings\": [\n" - + " {\n" - + " \"scenarioName\": \"OAuth token request error\",\n" - + " \"requiredScenarioState\": \"Started\",\n" - + " \"newScenarioState\": \"Authorized\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/authorize.*\",\n" - + " \"method\": \"GET\"\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 200\n" - + " },\n" - + " \"serveEventListeners\": [\n" - + " {\n" - + " \"name\": \"webhook\",\n" - + " \"parameters\": {\n" - + " \"method\": \"GET\",\n" - + " \"url\": \"http://localhost:8003/snowflake/oauth-redirect?code=123\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " },\n" - + " {\n" - + " \"scenarioName\": \"OAuth token request error\",\n" - + " \"requiredScenarioState\": \"Authorized\",\n" - + " \"newScenarioState\": \"Token request error\",\n" - + " \"request\": {\n" - + " \"urlPathPattern\": \"/oauth/token-request.*\",\n" - + " \"method\": \"POST\",\n" - + " \"headers\": {\n" - + " \"Authorization\": {\n" - + " \"contains\": \"Basic\"\n" - + " },\n" - + " \"Content-Type\": {\n" - + " \"contains\": \"application/x-www-form-urlencoded; charset=UTF-8\"\n" - + " }\n" - + " },\n" - + " \"bodyPatterns\": [{\n" - + " \"contains\": \"grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8003%2Fsnowflake%2Foauth-redirect&code_verifier=\"\n" - + " }]\n" - + " },\n" - + " \"response\": {\n" - + " \"status\": 400\n" - + " }\n" - + " }\n" - + " ],\n" - + " \"importOptions\": {\n" - + " \"duplicatePolicy\": \"IGNORE\",\n" - + " \"deleteAllNotInImport\": true\n" - + " }\n" - + "}"; + SCENARIOS_BASE_DIR + "/token_request_error_scenario_mapping.json"; private static final Logger logger = LoggerFactory.getLogger(OauthAuthorizationCodeFlowLatestIT.class); - AuthExternalBrowserHandlers wiremockProxyRequestBrowserHandler = + private final AuthExternalBrowserHandlers wiremockProxyRequestBrowserHandler = new WiremockProxyRequestBrowserHandler(); @Test public void successfulFlowScenario() throws SFException { - importMapping(SUCCESSFUL_FLOW_SCENARIO_MAPPINGS); + importMappingFromResources(SUCCESSFUL_FLOW_SCENARIO_MAPPINGS); SFLoginInput loginInput = createLoginInputStub("http://localhost:8001/snowflake/oauth-redirect"); @@ -208,7 +59,7 @@ public void successfulFlowScenario() throws SFException { @Test public void browserTimeoutFlowScenario() { - importMapping(BROWSER_TIMEOUT_SCENARIO_MAPPING); + importMappingFromResources(BROWSER_TIMEOUT_SCENARIO_MAPPING); SFLoginInput loginInput = createLoginInputStub("http://localhost:8004/snowflake/oauth-redirect"); @@ -224,7 +75,7 @@ public void browserTimeoutFlowScenario() { @Test public void invalidScopeFlowScenario() { - importMapping(INVALID_SCOPE_SCENARIO_MAPPING); + importMappingFromResources(INVALID_SCOPE_SCENARIO_MAPPING); SFLoginInput loginInput = createLoginInputStub("http://localhost:8002/snowflake/oauth-redirect"); @@ -240,7 +91,7 @@ public void invalidScopeFlowScenario() { @Test public void tokenRequestErrorFlowScenario() { - importMapping(TOKEN_REQUEST_ERROR_SCENARIO_MAPPING); + importMappingFromResources(TOKEN_REQUEST_ERROR_SCENARIO_MAPPING); SFLoginInput loginInput = createLoginInputStub("http://localhost:8003/snowflake/oauth-redirect"); diff --git a/src/test/resources/oauth/authorization_code/browser_timeout_scenario_mapping.json b/src/test/resources/oauth/authorization_code/browser_timeout_scenario_mapping.json new file mode 100644 index 000000000..41b5c8045 --- /dev/null +++ b/src/test/resources/oauth/authorization_code/browser_timeout_scenario_mapping.json @@ -0,0 +1,16 @@ +{ + "mappings": [ + { + "scenarioName": "Browser Authorization timeout", + "request": { + "urlPathPattern": "/oauth/authorize.*", + "method": "GET" + }, + "response": { + "status": 200, + "fixedDelayMilliseconds": 5000 + } + } + ] +} + \ No newline at end of file diff --git a/src/test/resources/oauth/authorization_code/invalid_scope_scenario_mapping.json b/src/test/resources/oauth/authorization_code/invalid_scope_scenario_mapping.json new file mode 100644 index 000000000..47bbd2064 --- /dev/null +++ b/src/test/resources/oauth/authorization_code/invalid_scope_scenario_mapping.json @@ -0,0 +1,23 @@ +{ + "mappings": [ + { + "scenarioName": "Invalid scope authorization error", + "request": { + "urlPathPattern": "/oauth/authorize.*", + "method": "GET" + }, + "response": { + "status": 200 + }, + "serveEventListeners": [ + { + "name": "webhook", + "parameters": { + "method": "GET", + "url": "http://localhost:8002/snowflake/oauth-redirect?error=invalid_scope&error_description=One+or+more+scopes+are+not+configured+for+the+authorization+server+resource." + } + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/oauth/authorization_code/successful_scenario_mapping.json b/src/test/resources/oauth/authorization_code/successful_scenario_mapping.json new file mode 100644 index 000000000..755441e23 --- /dev/null +++ b/src/test/resources/oauth/authorization_code/successful_scenario_mapping.json @@ -0,0 +1,51 @@ +{ + "mappings": [ + { + "scenarioName": "Successful OAuth authorization code flow", + "requiredScenarioState": "Started", + "newScenarioState": "Authorized", + "request": { + "urlPathPattern": "/oauth/authorize.*", + "method": "GET" + }, + "response": { + "status": 200 + }, + "serveEventListeners": [ + { + "name": "webhook", + "parameters": { + "method": "GET", + "url": "http://localhost:8001/snowflake/oauth-redirect?code=123" + } + } + ] + }, + { + "scenarioName": "Successful OAuth authorization code flow", + "requiredScenarioState": "Authorized", + "newScenarioState": "Acquired access token", + "request": { + "urlPathPattern": "/oauth/token-request.*", + "method": "POST", + "headers": { + "Authorization": { + "contains": "Basic" + }, + "Content-Type": { + "contains": "application/x-www-form-urlencoded; charset=UTF-8" + } + }, + "bodyPatterns": [ + { + "contains": "grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8001%2Fsnowflake%2Foauth-redirect&code_verifier=" + } + ] + }, + "response": { + "status": 200, + "body": "{ \"access_token\" : \"access-token-123\", \"refresh_token\" : \"123\", \"token_type\" : \"Bearer\", \"username\" : \"user\", \"scope\" : \"refresh_token session:role:ANALYST\", \"expires_in\" : 600, \"refresh_token_expires_in\" : 86399, \"idpInitiated\" : false }" + } + } + ] +} diff --git a/src/test/resources/oauth/authorization_code/token_request_error_scenario_mapping.json b/src/test/resources/oauth/authorization_code/token_request_error_scenario_mapping.json new file mode 100644 index 000000000..c0fe106b9 --- /dev/null +++ b/src/test/resources/oauth/authorization_code/token_request_error_scenario_mapping.json @@ -0,0 +1,50 @@ +{ + "mappings": [ + { + "scenarioName": "OAuth token request error", + "requiredScenarioState": "Started", + "newScenarioState": "Authorized", + "request": { + "urlPathPattern": "/oauth/authorize.*", + "method": "GET" + }, + "response": { + "status": 200 + }, + "serveEventListeners": [ + { + "name": "webhook", + "parameters": { + "method": "GET", + "url": "http://localhost:8003/snowflake/oauth-redirect?code=123" + } + } + ] + }, + { + "scenarioName": "OAuth token request error", + "requiredScenarioState": "Authorized", + "newScenarioState": "Token request error", + "request": { + "urlPathPattern": "/oauth/token-request.*", + "method": "POST", + "headers": { + "Authorization": { + "contains": "Basic" + }, + "Content-Type": { + "contains": "application/x-www-form-urlencoded; charset=UTF-8" + } + }, + "bodyPatterns": [ + { + "contains": "grant_type=authorization_code&code=123&redirect_uri=http%3A%2F%2Flocalhost%3A8003%2Fsnowflake%2Foauth-redirect&code_verifier=" + } + ] + }, + "response": { + "status": 400 + } + } + ] +} \ No newline at end of file From 77a9ce0f4f2be1cffe6c51aa0fa0893738d17167 Mon Sep 17 00:00:00 2001 From: Dawid Heyman Date: Thu, 5 Dec 2024 10:32:48 +0100 Subject: [PATCH 13/39] linkage checker --- linkage-checker-exclusion-rules.xml | 30 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/linkage-checker-exclusion-rules.xml b/linkage-checker-exclusion-rules.xml index 64b5860c2..9efccb30f 100644 --- a/linkage-checker-exclusion-rules.xml +++ b/linkage-checker-exclusion-rules.xml @@ -14,21 +14,16 @@ Optional - - - - Optional - provided appengine - - - - ? - + + + + + @@ -49,6 +44,21 @@ ? + + + + ? + + + + + ? + + + + + ? + - - - - + + + + ? + + @@ -59,6 +60,11 @@ ? + + + + ? +