Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snow 1925254 authentication logic refactor #2073

Merged
merged 11 commits into from
Feb 17, 2025
8 changes: 4 additions & 4 deletions src/main/java/net/snowflake/client/core/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ public static boolean isSocksProxyDisabled() {
}

/**
* Executes a HTTP request with the cookie spec set to IGNORE_COOKIES
* Executes an HTTP request with the cookie spec set to IGNORE_COOKIES
*
* @param httpRequest HttpRequestBase
* @param retryTimeout retry timeout
Expand Down Expand Up @@ -622,7 +622,7 @@ static String executeRequestWithoutCookies(
}

/**
* Executes a HTTP request for Snowflake.
* Executes an HTTP request for Snowflake.
*
* @param httpRequest HttpRequestBase
* @param retryTimeout retry timeout
Expand Down Expand Up @@ -658,7 +658,7 @@ public static String executeGeneralRequest(
}

/**
* Executes a HTTP request for Snowflake
* Executes an HTTP request for Snowflake
*
* @param httpRequest HttpRequestBase
* @param retryTimeout retry timeout
Expand Down Expand Up @@ -696,7 +696,7 @@ public static String executeGeneralRequest(
}

/**
* Executes a HTTP request for Snowflake.
* Executes an HTTP request for Snowflake.
*
* @param httpRequest HttpRequestBase
* @param retryTimeout retry timeout
Expand Down
237 changes: 151 additions & 86 deletions src/main/java/net/snowflake/client/core/SessionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static net.snowflake.client.core.SFTrustManager.resetOCSPResponseCacherServerURL;
import static net.snowflake.client.jdbc.SnowflakeUtil.systemGetProperty;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
Expand Down Expand Up @@ -271,7 +272,7 @@ static SFLoginOutput openSession(
AssertUtil.assertTrue(
loginInput.getUserName() != null, "missing user name for opening session");
} else {
// OAUTH needs either token or passord
// OAUTH needs either token or password
AssertUtil.assertTrue(
loginInput.getToken() != null || loginInput.getPassword() != null,
"missing token or password for opening session");
Expand Down Expand Up @@ -707,7 +708,7 @@ private static SFLoginOutput newSession(
// In RestRequest.execute(), socket timeout is replaced with auth timeout
// so we can renew the request within auth timeout.
// auth timeout within socket timeout is thrown without backoff,
// and we need to update time remained in socket timeout here to control the
// and we need to update time remained in socket timeout here to control
// the actual socket timeout from customer setting.
if (loginInput.getSocketTimeoutInMillis() > 0) {
if (ex.issocketTimeoutNoBackoff()) {
Expand Down Expand Up @@ -737,29 +738,7 @@ private static SFLoginOutput newSession(
break;
}

if (theString == null) {
if (lastRestException != null) {
logger.error(
"Failed to open new session for user: {}, host: {}. Error: {}",
loginInput.getUserName(),
loginInput.getHostFromServerUrl(),
lastRestException);
throw lastRestException;
} else {
SnowflakeSQLException exception =
new SnowflakeSQLException(
NO_QUERY_ID,
"empty authentication response",
SqlState.CONNECTION_EXCEPTION,
ErrorCode.CONNECTION_ERROR.getMessageCode());
logger.error(
"Failed to open new session for user: {}, host: {}. Error: {}",
loginInput.getUserName(),
loginInput.getHostFromServerUrl(),
exception);
throw exception;
}
}
handleEmptyAuthResponse(theString, loginInput, lastRestException);

// general method, same as with data binding
JsonNode jsonNode = mapper.readTree(theString);
Expand Down Expand Up @@ -1201,22 +1180,11 @@ static void closeSession(SFLoginInput loginInput) throws SFException, SnowflakeS
private static String federatedFlowStep4(
SFLoginInput loginInput, String ssoUrl, String oneTimeToken) throws SnowflakeSQLException {
String responseHtml = "";
try {

final URL url = new URL(ssoUrl);
URI oktaGetUri =
new URIBuilder()
.setScheme(url.getProtocol())
.setHost(url.getHost())
.setPath(url.getPath())
.setParameter("RelayState", "%2Fsome%2Fdeep%2Flink")
.setParameter("onetimetoken", oneTimeToken)
.build();
HttpGet httpGet = new HttpGet(oktaGetUri);
try {

HeaderGroup headers = new HeaderGroup();
headers.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
httpGet.setHeaders(headers.getAllHeaders());
HttpGet httpGet = new HttpGet();
prepareFederatedFlowStep4Request(httpGet, ssoUrl, oneTimeToken);

responseHtml =
HttpUtil.executeGeneralRequest(
Expand Down Expand Up @@ -1276,26 +1244,7 @@ private static String federatedFlowStep3(SFLoginInput loginInput, String tokenUr
URL url = new URL(tokenUrl);
URI tokenUri = url.toURI();
final HttpPost postRequest = new HttpPost(tokenUri);

String userName;
if (Strings.isNullOrEmpty(loginInput.getOKTAUserName())) {
userName = loginInput.getUserName();
} else {
userName = loginInput.getOKTAUserName();
}
StringEntity params =
new StringEntity(
"{\"username\":\""
+ userName
+ "\",\"password\":\""
+ loginInput.getPassword()
+ "\"}");
postRequest.setEntity(params);

HeaderGroup headers = new HeaderGroup();
headers.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
headers.addHeader(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
postRequest.setHeaders(headers.getAllHeaders());
setFederatedFlowStep3PostRequestAuthData(postRequest, loginInput);

final String idpResponse =
HttpUtil.executeRequestWithoutCookies(
Expand Down Expand Up @@ -1363,29 +1312,8 @@ private static void federatedFlowStep2(SFLoginInput loginInput, String tokenUrl,
private static JsonNode federatedFlowStep1(SFLoginInput loginInput) throws SnowflakeSQLException {
JsonNode dataNode = null;
try {
URIBuilder fedUriBuilder = new URIBuilder(loginInput.getServerUrl());
fedUriBuilder.setPath(SF_PATH_AUTHENTICATOR_REQUEST);
URI fedUrlUri = fedUriBuilder.build();

Map<String, Object> data = new HashMap<>();
data.put(ClientAuthnParameter.ACCOUNT_NAME.name(), loginInput.getAccountName());
data.put(ClientAuthnParameter.AUTHENTICATOR.name(), loginInput.getAuthenticator());
data.put(ClientAuthnParameter.CLIENT_APP_ID.name(), loginInput.getAppId());
data.put(ClientAuthnParameter.CLIENT_APP_VERSION.name(), loginInput.getAppVersion());

ClientAuthnDTO authnData = new ClientAuthnDTO(data, null);
String json = mapper.writeValueAsString(authnData);

// attach the login info json body to the post request
StringEntity input = new StringEntity(json, StandardCharsets.UTF_8);
input.setContentType("application/json");
HttpPost postRequest = new HttpPost(fedUrlUri);
postRequest.setEntity(input);
postRequest.addHeader("accept", "application/json");

// Add headers for driver name and version
postRequest.addHeader(SF_HEADER_CLIENT_APP_ID, loginInput.getAppId());
postRequest.addHeader(SF_HEADER_CLIENT_APP_VERSION, loginInput.getAppVersion());
StringEntity requestInput = prepareFederatedFlowStep1RequestInput(loginInput);
HttpPost postRequest = prepareFederatedFlowStep1PostRequest(loginInput, requestInput);

final String gsResponse =
HttpUtil.executeGeneralRequest(
Expand All @@ -1395,6 +1323,7 @@ private static JsonNode federatedFlowStep1(SFLoginInput loginInput) throws Snowf
loginInput.getSocketTimeoutInMillis(),
0,
loginInput.getHttpClientSettingsKey());

logger.debug("Authenticator-request response: {}", gsResponse);
JsonNode jsonNode = mapper.readTree(gsResponse);

Expand Down Expand Up @@ -1790,12 +1719,148 @@ public static boolean isNewRetryStrategyRequest(HttpRequestBase request) {
URI requestURI = request.getURI();
String requestPath = requestURI.getPath();
if (requestPath != null) {
if (requestPath.equals(SF_PATH_LOGIN_REQUEST)
return requestPath.equals(SF_PATH_LOGIN_REQUEST)
|| requestPath.equals(SF_PATH_AUTHENTICATOR_REQUEST)
|| requestPath.equals(SF_PATH_TOKEN_REQUEST)) {
return true;
}
|| requestPath.equals(SF_PATH_TOKEN_REQUEST);
}
return false;
}

/**
* Prepares an HTTP POST request for the first step of the federated authentication flow.
*
* @param loginInput The login information for the request.
* @param inputData The JSON input data to include in the request.
* @return An {@link HttpPost} object ready to execute the federated flow request.
* @throws URISyntaxException If the constructed URI is invalid.
*/
private static HttpPost prepareFederatedFlowStep1PostRequest(
SFLoginInput loginInput, StringEntity inputData) throws URISyntaxException {
URIBuilder fedUriBuilder = new URIBuilder(loginInput.getServerUrl());
// TODO: if loginInput.serverUrl contains port or additional segments - it will be ignored and
// overwritten here - to be fixed in SNOW-1922872
fedUriBuilder.setPath(SF_PATH_AUTHENTICATOR_REQUEST);
URI fedUrlUri = fedUriBuilder.build();

HttpPost postRequest = new HttpPost(fedUrlUri);
postRequest.setEntity(inputData);
postRequest.addHeader("accept", "application/json");

postRequest.addHeader(SF_HEADER_CLIENT_APP_ID, loginInput.getAppId());
postRequest.addHeader(SF_HEADER_CLIENT_APP_VERSION, loginInput.getAppVersion());

return postRequest;
}

/**
* Prepares the JSON input for the first step of the federated authentication flow.
*
* @param loginInput The login information for the request.
* @return A {@link StringEntity} containing the JSON input for the request.
* @throws JsonProcessingException If there is an error generating the JSON input.
*/
private static StringEntity prepareFederatedFlowStep1RequestInput(SFLoginInput loginInput)
throws JsonProcessingException {
Map<String, Object> data = new HashMap<>();
data.put(ClientAuthnParameter.ACCOUNT_NAME.name(), loginInput.getAccountName());
data.put(ClientAuthnParameter.AUTHENTICATOR.name(), loginInput.getAuthenticator());
data.put(ClientAuthnParameter.CLIENT_APP_ID.name(), loginInput.getAppId());
data.put(ClientAuthnParameter.CLIENT_APP_VERSION.name(), loginInput.getAppVersion());

ClientAuthnDTO authnData = new ClientAuthnDTO(data, null);
String json = mapper.writeValueAsString(authnData);

StringEntity input = new StringEntity(json, StandardCharsets.UTF_8);
input.setContentType("application/json");
return input;
}

/**
* Sets the authentication data for the third step of the federated authentication flow.
*
* @param postRequest The {@link HttpPost} request to update with authentication data.
* @param loginInput The login information for the request.
* @throws SnowflakeSQLException If an error occurs while preparing the request.
*/
private static void setFederatedFlowStep3PostRequestAuthData(
HttpPost postRequest, SFLoginInput loginInput) throws SnowflakeSQLException {
String userName =
Strings.isNullOrEmpty(loginInput.getOKTAUserName())
? loginInput.getUserName()
: loginInput.getOKTAUserName();
try {
StringEntity params =
new StringEntity(
"{\"username\":\""
+ userName
+ "\",\"password\":\""
+ loginInput.getPassword()
+ "\"}");
postRequest.setEntity(params);

HeaderGroup headers = new HeaderGroup();
headers.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
headers.addHeader(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
postRequest.setHeaders(headers.getAllHeaders());
} catch (IOException ex) {
handleFederatedFlowError(loginInput, ex);
}
}

/**
* Prepares an HTTP GET request for the fourth step of the federated authentication flow.
*
* @param retrieveSamlRequest The {@link HttpRequestBase} to update with the SAML request details.
* @param ssoUrl The SSO URL to use for the request.
* @param oneTimeToken The one-time token to include in the request.
* @throws MalformedURLException If the SSO URL is malformed.
* @throws URISyntaxException If the URI for the request cannot be built.
*/
private static void prepareFederatedFlowStep4Request(
HttpRequestBase retrieveSamlRequest, String ssoUrl, String oneTimeToken)
throws MalformedURLException, URISyntaxException {
final URL url = new URL(ssoUrl);
URI oktaGetUri =
new URIBuilder()
.setScheme(url.getProtocol())
.setHost(url.getHost())
.setPort(url.getPort())
.setPath(url.getPath())
.setParameter("RelayState", "%2Fsome%2Fdeep%2Flink")
.setParameter("onetimetoken", oneTimeToken)
.build();
retrieveSamlRequest.setURI(oktaGetUri);

HeaderGroup headers = new HeaderGroup();
headers.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "*/*"));
retrieveSamlRequest.setHeaders(headers.getAllHeaders());
}

private static void handleEmptyAuthResponse(
String theString, SFLoginInput loginInput, Exception lastRestException)
throws Exception, SFException {
if (theString == null) {
if (lastRestException != null) {
logger.error(
"Failed to open new session for user: {}, host: {}. Error: {}",
loginInput.getUserName(),
loginInput.getHostFromServerUrl(),
lastRestException);
throw lastRestException;
} else {
SnowflakeSQLException exception =
new SnowflakeSQLException(
NO_QUERY_ID,
"empty authentication response",
SqlState.CONNECTION_EXCEPTION,
ErrorCode.CONNECTION_ERROR.getMessageCode());
logger.error(
"Failed to open new session for user: {}, host: {}. Error: {}",
loginInput.getUserName(),
loginInput.getHostFromServerUrl(),
exception);
throw exception;
}
}
}
}
Loading
Loading