All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.snowflake.client.core.SessionUtil Maven / Gradle / Ivy

/*
 * Copyright (c) 2012-2022 Snowflake Computing Inc. All rights reserved.
 */

package net.snowflake.client.core;

import static net.snowflake.client.core.SFTrustManager.resetOCSPResponseCacherServerURL;
import static net.snowflake.client.jdbc.SnowflakeUtil.systemGetProperty;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.snowflake.client.jdbc.*;
import net.snowflake.client.jdbc.telemetryOOB.TelemetryService;
import net.snowflake.client.log.ArgSupplier;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import net.snowflake.client.util.SecretDetector;
import net.snowflake.common.core.ClientAuthnDTO;
import net.snowflake.common.core.ClientAuthnParameter;
import net.snowflake.common.core.SqlState;
import org.apache.http.HttpHeaders;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.HeaderGroup;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

/** Low level session util */
public class SessionUtil {
  private static final SFLogger logger = SFLoggerFactory.getLogger(SessionUtil.class);
  // Response Field Name
  private static final String SF_QUERY_DATABASE = "databaseName";
  private static final String SF_QUERY_SCHEMA = "schemaName";
  private static final String SF_QUERY_WAREHOUSE = "warehouse";
  private static final String SF_QUERY_ROLE = "roleName";

  // Request path
  private static final String SF_PATH_LOGIN_REQUEST = "/session/v1/login-request";
  private static final String SF_PATH_TOKEN_REQUEST = "/session/token-request";
  public static final String SF_PATH_AUTHENTICATOR_REQUEST = "/session/authenticator-request";

  public static final String SF_QUERY_SESSION_DELETE = "delete";

  // Headers
  public static final String SF_HEADER_AUTHORIZATION = HttpHeaders.AUTHORIZATION;

  // Authentication type
  private static final String SF_HEADER_BASIC_AUTHTYPE = "Basic";
  private static final String SF_HEADER_SNOWFLAKE_AUTHTYPE = "Snowflake";
  private static final String SF_HEADER_TOKEN_TAG = "Token";
  private static final String CLIENT_STORE_TEMPORARY_CREDENTIAL =
      "CLIENT_STORE_TEMPORARY_CREDENTIAL";
  private static final String CLIENT_REQUEST_MFA_TOKEN = "CLIENT_REQUEST_MFA_TOKEN";
  private static final String SERVICE_NAME = "SERVICE_NAME";
  private static final String CLIENT_IN_BAND_TELEMETRY_ENABLED = "CLIENT_TELEMETRY_ENABLED";
  private static final String CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED =
      "CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED";
  private static final String CLIENT_RESULT_COLUMN_CASE_INSENSITIVE =
      "CLIENT_RESULT_COLUMN_CASE_INSENSITIVE";
  private static final String JDBC_RS_COLUMN_CASE_INSENSITIVE = "JDBC_RS_COLUMN_CASE_INSENSITIVE";
  private static final String JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC = "JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC";
  private static final String JDBC_FORMAT_DATE_WITH_TIMEZONE = "JDBC_FORMAT_DATE_WITH_TIMEZONE";
  private static final String JDBC_USE_SESSION_TIMEZONE = "JDBC_USE_SESSION_TIMEZONE";
  public static final String JDBC_CHUNK_DOWNLOADER_MAX_RETRY = "JDBC_CHUNK_DOWNLOADER_MAX_RETRY";
  private static final String CLIENT_RESULT_CHUNK_SIZE_JVM =
      "net.snowflake.jdbc.clientResultChunkSize";
  public static final String CLIENT_RESULT_CHUNK_SIZE = "CLIENT_RESULT_CHUNK_SIZE";
  public static final String CLIENT_MEMORY_LIMIT_JVM = "net.snowflake.jdbc.clientMemoryLimit";
  public static final String CLIENT_MEMORY_LIMIT = "CLIENT_MEMORY_LIMIT";
  public static final String QUERY_CONTEXT_CACHE_SIZE = "QUERY_CONTEXT_CACHE_SIZE";
  public static final String JDBC_ENABLE_PUT_GET = "JDBC_ENABLE_PUT_GET";
  public static final String CLIENT_PREFETCH_THREADS_JVM =
      "net.snowflake.jdbc.clientPrefetchThreads";
  public static final String CLIENT_PREFETCH_THREADS = "CLIENT_PREFETCH_THREADS";
  public static final String CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE_JVM =
      "net.snowflake.jdbc.clientEnableConservativeMemoryUsage";
  public static final String CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE =
      "CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE";
  public static final String CLIENT_CONSERVATIVE_MEMORY_ADJUST_STEP =
      "CLIENT_CONSERVATIVE_MEMORY_ADJUST_STEP";
  public static final String OCSP_FAIL_OPEN_JVM = "net.snowflake.jdbc.ocspFailOpen";
  private static final String OCSP_FAIL_OPEN = "ocspFailOpen";
  public static final String CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY =
      "CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY";
  public static final String CLIENT_SFSQL = "CLIENT_SFSQL";
  public static final String CLIENT_VALIDATE_DEFAULT_PARAMETERS =
      "CLIENT_VALIDATE_DEFAULT_PARAMETERS";
  public static final String CLIENT_ENABLE_LOG_INFO_STATEMENT_PARAMETERS =
      "CLIENT_ENABLE_LOG_INFO_STATEMENT_PARAMETERS";
  public static final String CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX =
      "CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX";
  public static final String CLIENT_METADATA_USE_SESSION_DATABASE =
      "CLIENT_METADATA_USE_SESSION_DATABASE";
  public static final String ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1 =
      "ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1";

  static final String SF_HEADER_SERVICE_NAME = "X-Snowflake-Service";

  public static final String SF_HEADER_CLIENT_APP_ID = "CLIENT_APP_ID";

  public static final String SF_HEADER_CLIENT_APP_VERSION = "CLIENT_APP_VERSION";

  private static final String ID_TOKEN_AUTHENTICATOR = "ID_TOKEN";

  private static final String NO_QUERY_ID = "";
  private static final String SF_PATH_SESSION = "/session";
  public static long DEFAULT_CLIENT_MEMORY_LIMIT = 1536; // MB
  public static int DEFAULT_CLIENT_PREFETCH_THREADS = 4;
  public static int MIN_CLIENT_CHUNK_SIZE = 48;
  public static int MAX_CLIENT_CHUNK_SIZE = 160;
  public static Map JVM_PARAMS_TO_PARAMS =
      Stream.of(
              new String[][] {
                {CLIENT_RESULT_CHUNK_SIZE_JVM, CLIENT_RESULT_CHUNK_SIZE},
                {CLIENT_MEMORY_LIMIT_JVM, CLIENT_MEMORY_LIMIT},
                {CLIENT_PREFETCH_THREADS_JVM, CLIENT_PREFETCH_THREADS},
                {OCSP_FAIL_OPEN_JVM, OCSP_FAIL_OPEN},
                {
                  CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE_JVM,
                  CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE
                }
              })
          .collect(Collectors.toMap(data -> data[0], data -> data[1]));
  private static ObjectMapper mapper = ObjectMapperFactory.getObjectMapper();
  private static int DEFAULT_HEALTH_CHECK_INTERVAL = 45; // sec
  private static Set STRING_PARAMS =
      new HashSet<>(
          Arrays.asList(
              "TIMEZONE",
              "TIMESTAMP_OUTPUT_FORMAT",
              "TIMESTAMP_NTZ_OUTPUT_FORMAT",
              "TIMESTAMP_LTZ_OUTPUT_FORMAT",
              "TIMESTAMP_TZ_OUTPUT_FORMAT",
              "DATE_OUTPUT_FORMAT",
              "TIME_OUTPUT_FORMAT",
              "BINARY_OUTPUT_FORMAT",
              "CLIENT_TIMESTAMP_TYPE_MAPPING",
              SERVICE_NAME,
              "GEOGRAPHY_OUTPUT_FORMAT"));
  private static final Set INT_PARAMS =
      new HashSet<>(
          Arrays.asList(
              CLIENT_PREFETCH_THREADS,
              CLIENT_MEMORY_LIMIT,
              CLIENT_RESULT_CHUNK_SIZE,
              "CLIENT_STAGE_ARRAY_BINDING_THRESHOLD",
              "CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY"));
  private static final Set BOOLEAN_PARAMS =
      new HashSet<>(
          Arrays.asList(
              CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY,
              "CLIENT_HONOR_CLIENT_TZ_FOR_TIMESTAMP_NTZ",
              "CLIENT_DISABLE_INCIDENTS",
              "CLIENT_SESSION_KEEP_ALIVE",
              CLIENT_ENABLE_LOG_INFO_STATEMENT_PARAMETERS,
              CLIENT_IN_BAND_TELEMETRY_ENABLED,
              CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED,
              CLIENT_STORE_TEMPORARY_CREDENTIAL,
              CLIENT_REQUEST_MFA_TOKEN,
              "JDBC_USE_JSON_PARSER",
              "AUTOCOMMIT",
              "JDBC_EFFICIENT_CHUNK_STORAGE",
              JDBC_RS_COLUMN_CASE_INSENSITIVE,
              JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC,
              JDBC_FORMAT_DATE_WITH_TIMEZONE,
              JDBC_USE_SESSION_TIMEZONE,
              CLIENT_RESULT_COLUMN_CASE_INSENSITIVE,
              CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX,
              CLIENT_METADATA_USE_SESSION_DATABASE,
              "JDBC_TREAT_DECIMAL_AS_INT",
              "JDBC_ENABLE_COMBINED_DESCRIBE",
              CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE,
              CLIENT_VALIDATE_DEFAULT_PARAMETERS,
              ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1,
              "SNOWPARK_LAZY_ANALYSIS"));

  /**
   * Returns Authenticator type
   *
   * @param loginInput login information
   * @return Authenticator type
   */
  private static ClientAuthnDTO.AuthenticatorType getAuthenticator(SFLoginInput loginInput) {
    if (loginInput.getAuthenticator() != null) {
      if (loginInput
          .getAuthenticator()
          .equalsIgnoreCase(ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER.name())) {
        // SAML 2.0 compliant service/application
        return ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER;
      } else if (loginInput
          .getAuthenticator()
          .equalsIgnoreCase(ClientAuthnDTO.AuthenticatorType.OAUTH.name())) {
        // OAuth Authentication
        return ClientAuthnDTO.AuthenticatorType.OAUTH;
      } else if (loginInput
          .getAuthenticator()
          .equalsIgnoreCase(ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT.name())) {
        return ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT;
      } else if (loginInput
          .getAuthenticator()
          .equalsIgnoreCase(ClientAuthnDTO.AuthenticatorType.USERNAME_PASSWORD_MFA.name())) {
        return ClientAuthnDTO.AuthenticatorType.USERNAME_PASSWORD_MFA;
      } else if (!loginInput
          .getAuthenticator()
          .equalsIgnoreCase(ClientAuthnDTO.AuthenticatorType.SNOWFLAKE.name())) {
        // OKTA authenticator v1.
        return ClientAuthnDTO.AuthenticatorType.OKTA;
      }
    }

    // authenticator is null, then jdbc will decide authenticator depends on
    // if privateKey is specified or not. If yes, authenticator type will be
    // SNOWFLAKE_JWT, otherwise it will use SNOWFLAKE.
    return (loginInput.getPrivateKey() != null || loginInput.getPrivateKeyFile() != null)
        ? ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT
        : ClientAuthnDTO.AuthenticatorType.SNOWFLAKE;
  }

  /**
   * Open a new session
   *
   * @param loginInput login information
   * @return information get after login such as token information
   * @throws SFException if unexpected uri syntax
   * @throws SnowflakeSQLException if failed to establish connection with snowflake
   */
  static SFLoginOutput openSession(
      SFLoginInput loginInput,
      Map connectionPropertiesMap,
      String tracingLevel)
      throws SFException, SnowflakeSQLException {
    AssertUtil.assertTrue(
        loginInput.getServerUrl() != null, "missing server URL for opening session");

    AssertUtil.assertTrue(loginInput.getAppId() != null, "missing app id for opening session");

    AssertUtil.assertTrue(
        loginInput.getLoginTimeout() >= 0, "negative login timeout for opening session");

    final ClientAuthnDTO.AuthenticatorType authenticator = getAuthenticator(loginInput);
    if (!authenticator.equals(ClientAuthnDTO.AuthenticatorType.OAUTH)) {
      // OAuth does not require a username
      AssertUtil.assertTrue(
          loginInput.getUserName() != null, "missing user name for opening session");
    } else {
      // OAUTH needs either token or passord
      AssertUtil.assertTrue(
          loginInput.getToken() != null || loginInput.getPassword() != null,
          "missing token or password for opening session");
    }
    if (authenticator.equals(ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER)) {
      if (Constants.getOS() == Constants.OS.MAC || Constants.getOS() == Constants.OS.WINDOWS) {
        // force to set the flag for Mac/Windows users
        loginInput.getSessionParameters().put(CLIENT_STORE_TEMPORARY_CREDENTIAL, true);
      } else {
        // Linux should read from JDBC configuration. For other unsupported OS, we set it to false
        // as default value
        if (!loginInput.getSessionParameters().containsKey(CLIENT_STORE_TEMPORARY_CREDENTIAL)) {
          loginInput.getSessionParameters().put(CLIENT_STORE_TEMPORARY_CREDENTIAL, false);
        }
      }
    } else {
      // TODO: patch for now. We should update mergeProperties
      // to normalize all parameters using STRING_PARAMS, INT_PARAMS and
      // BOOLEAN_PARAMS.
      Object value = loginInput.getSessionParameters().get(CLIENT_STORE_TEMPORARY_CREDENTIAL);
      if (value != null) {
        loginInput.getSessionParameters().put(CLIENT_STORE_TEMPORARY_CREDENTIAL, asBoolean(value));
      }
    }

    if (authenticator.equals(ClientAuthnDTO.AuthenticatorType.USERNAME_PASSWORD_MFA)) {
      if (Constants.getOS() == Constants.OS.MAC || Constants.getOS() == Constants.OS.WINDOWS) {
        loginInput.getSessionParameters().put(CLIENT_REQUEST_MFA_TOKEN, true);
      }
    }

    preNewSession(loginInput);

    try {
      return newSession(loginInput, connectionPropertiesMap, tracingLevel);
    } catch (SnowflakeReauthenticationRequest ex) {
      // Id Token expired. We run newSession again with id_token cache cleared
      logger.debug("ID Token being used has expired. Reauthenticating with ID Token cleared...");
      return newSession(loginInput, connectionPropertiesMap, tracingLevel);
    }
  }

  private static void preNewSession(SFLoginInput loginInput) throws SFException {
    if (asBoolean(loginInput.getSessionParameters().get(CLIENT_STORE_TEMPORARY_CREDENTIAL))) {
      CredentialManager.getInstance().fillCachedIdToken(loginInput);
    }

    if (asBoolean(loginInput.getSessionParameters().get(CLIENT_REQUEST_MFA_TOKEN))) {
      CredentialManager.getInstance().fillCachedMfaToken(loginInput);
    }
  }

  private static boolean asBoolean(Object value) {
    if (value == null) {
      return false;
    }
    switch (value.getClass().getName()) {
      case "java.lang.Boolean":
        return (Boolean) value;
      case "java.lang.String":
        return Boolean.valueOf((String) value);
    }
    return false;
  }

  private static SFLoginOutput newSession(
      SFLoginInput loginInput,
      Map connectionPropertiesMap,
      String tracingLevel)
      throws SFException, SnowflakeSQLException {
    // build URL for login request
    URIBuilder uriBuilder;
    URI loginURI;
    String tokenOrSamlResponse = null;
    String samlProofKey = null;
    boolean consentCacheIdToken = true;

    String sessionToken;
    String masterToken;
    String sessionDatabase;
    String sessionSchema;
    String sessionRole;
    String sessionWarehouse;
    String sessionId;
    long masterTokenValidityInSeconds;
    String idToken;
    String mfaToken;
    String databaseVersion = null;
    int databaseMajorVersion = 0;
    int databaseMinorVersion = 0;
    String newClientForUpgrade;
    int healthCheckInterval = DEFAULT_HEALTH_CHECK_INTERVAL;
    int httpClientSocketTimeout = loginInput.getSocketTimeout();
    final ClientAuthnDTO.AuthenticatorType authenticatorType = getAuthenticator(loginInput);
    Map commonParams;

    try {

      uriBuilder = new URIBuilder(loginInput.getServerUrl());
      // add database name and schema name as query parameters
      if (loginInput.getDatabaseName() != null) {
        uriBuilder.addParameter(SF_QUERY_DATABASE, loginInput.getDatabaseName());
      }

      if (loginInput.getSchemaName() != null) {
        uriBuilder.addParameter(SF_QUERY_SCHEMA, loginInput.getSchemaName());
      }

      if (loginInput.getWarehouse() != null) {
        uriBuilder.addParameter(SF_QUERY_WAREHOUSE, loginInput.getWarehouse());
      }

      if (loginInput.getRole() != null) {
        uriBuilder.addParameter(SF_QUERY_ROLE, loginInput.getRole());
      }

      if (authenticatorType == ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER) {
        // try to reuse id_token if exists
        if (loginInput.getIdToken() == null) {
          // SAML 2.0 compliant service/application
          SessionUtilExternalBrowser s = SessionUtilExternalBrowser.createInstance(loginInput);
          s.authenticate();
          tokenOrSamlResponse = s.getToken();
          samlProofKey = s.getProofKey();
          consentCacheIdToken = s.isConsentCacheIdToken();
        }
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.OKTA) {
        // okta authenticator v1
        tokenOrSamlResponse = getSamlResponseUsingOkta(loginInput);
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT) {
        SessionUtilKeyPair s =
            new SessionUtilKeyPair(
                loginInput.getPrivateKey(),
                loginInput.getPrivateKeyFile(),
                loginInput.getPrivateKeyFilePwd(),
                loginInput.getAccountName(),
                loginInput.getUserName());

        loginInput.setToken(s.issueJwtToken());
        loginInput.setAuthTimeout(SessionUtilKeyPair.getTimeout());
      }

      uriBuilder.addParameter(SFSession.SF_QUERY_REQUEST_ID, UUIDUtils.getUUID().toString());

      uriBuilder.setPath(SF_PATH_LOGIN_REQUEST);
      loginURI = uriBuilder.build();
    } catch (URISyntaxException ex) {
      logger.error("Exception when building URL", ex);

      throw new SFException(ex, ErrorCode.INTERNAL_ERROR, "unexpected URI syntax exception:1");
    }

    try {
      // Adjust OCSP cache server if it is private link
      resetOCSPUrlIfNecessary(loginInput.getServerUrl());
    } catch (IOException ex) {
      throw new SFException(ex, ErrorCode.IO_ERROR, "unexpected URL syntax exception");
    }

    HttpPost postRequest = null;

    try {
      ClientAuthnDTO authnData = new ClientAuthnDTO();
      authnData.setInFlightCtx(loginInput.getInFlightCtx());

      Map data = new HashMap<>();
      data.put(ClientAuthnParameter.CLIENT_APP_ID.name(), loginInput.getAppId());

      /*
       * username is always included regardless of authenticator to identify
       * the user.
       */
      data.put(ClientAuthnParameter.LOGIN_NAME.name(), loginInput.getUserName());

      /*
       * only include password information in the request to GS if federated
       * authentication method is not specified.
       * When specified, this password information is really to be used to
       * authenticate with the IDP provider only, and GS should not have any
       * trace for this information.
       */
      if (authenticatorType == ClientAuthnDTO.AuthenticatorType.SNOWFLAKE) {
        data.put(ClientAuthnParameter.PASSWORD.name(), loginInput.getPassword());
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER) {
        if (loginInput.getIdToken() != null) {
          data.put(ClientAuthnParameter.AUTHENTICATOR.name(), ID_TOKEN_AUTHENTICATOR);
          data.put(ClientAuthnParameter.TOKEN.name(), loginInput.getIdToken());
        } else {
          data.put(
              ClientAuthnParameter.AUTHENTICATOR.name(),
              ClientAuthnDTO.AuthenticatorType.EXTERNALBROWSER.name());
          data.put(ClientAuthnParameter.PROOF_KEY.name(), samlProofKey);
          data.put(ClientAuthnParameter.TOKEN.name(), tokenOrSamlResponse);
        }
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.OKTA) {
        data.put(ClientAuthnParameter.RAW_SAML_RESPONSE.name(), tokenOrSamlResponse);
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.OAUTH) {
        data.put(ClientAuthnParameter.AUTHENTICATOR.name(), authenticatorType.name());

        // Fix for HikariCP refresh token issue:SNOW-533673.
        // If token value is not set but password field is set then
        // the driver treats password as token.
        if (loginInput.getToken() != null)
          data.put(ClientAuthnParameter.TOKEN.name(), loginInput.getToken());
        else data.put(ClientAuthnParameter.TOKEN.name(), loginInput.getPassword());

      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT) {
        data.put(ClientAuthnParameter.AUTHENTICATOR.name(), authenticatorType.name());
        data.put(ClientAuthnParameter.TOKEN.name(), loginInput.getToken());
      } else if (authenticatorType == ClientAuthnDTO.AuthenticatorType.USERNAME_PASSWORD_MFA) {
        // No authenticator name should be added here, since this will be treated as snowflake
        // default authenticator by backend
        data.put(ClientAuthnParameter.PASSWORD.name(), loginInput.getPassword());
        if (loginInput.getMfaToken() != null) {
          data.put(ClientAuthnParameter.TOKEN.name(), loginInput.getMfaToken());
        }
      }

      // map of client environment parameters, including connection parameters
      // and environment properties like OS version, etc.
      Map clientEnv = new HashMap<>();

      clientEnv.put("OS", systemGetProperty("os.name"));
      clientEnv.put("OS_VERSION", systemGetProperty("os.version"));
      clientEnv.put("JAVA_VERSION", systemGetProperty("java.version"));
      clientEnv.put("JAVA_RUNTIME", systemGetProperty("java.runtime.name"));
      clientEnv.put("JAVA_VM", systemGetProperty("java.vm.name"));
      clientEnv.put("OCSP_MODE", loginInput.getOCSPMode().name());

      if (loginInput.getApplication() != null) {
        clientEnv.put("APPLICATION", loginInput.getApplication());
      } else {
        // When you add new client environment info, please add new keys to
        // messages_en_US.src.json so that they can be displayed properly in UI
        // detect app name
        String appName = systemGetProperty("sun.java.command");
        // remove the arguments
        if (appName != null) {
          if (appName.indexOf(" ") > 0) {
            appName = appName.substring(0, appName.indexOf(" "));
          }

          clientEnv.put("APPLICATION", appName);
        }
      }

      // SNOW-20103: track additional client info in session
      String clientInfoJSONStr;
      if (connectionPropertiesMap.containsKey(SFSessionProperty.CLIENT_INFO)) {
        clientInfoJSONStr = (String) connectionPropertiesMap.get(SFSessionProperty.CLIENT_INFO);
      }
      // if connection property is not set, check session property
      else {
        clientInfoJSONStr = systemGetProperty("snowflake.client.info");
      }
      if (clientInfoJSONStr != null) {
        JsonNode clientInfoJSON = null;

        try {
          clientInfoJSON = mapper.readTree(clientInfoJSONStr);
        } catch (Throwable ex) {
          logger.debug(
              "failed to process snowflake.client.info property as JSON: {}",
              clientInfoJSONStr,
              ex);
        }

        if (clientInfoJSON != null) {
          Iterator> fields = clientInfoJSON.fields();
          while (fields.hasNext()) {
            Map.Entry field = fields.next();
            clientEnv.put(field.getKey(), field.getValue().asText());
          }
        }
      }
      /*
       Add all connection parameters and their values that have been set for this
       * current session into clientEnv. These are the params set via the Properties map or in the
       * connection string. Includes username, password, serverUrl, timeout values, etc
      */

      for (Map.Entry entry : connectionPropertiesMap.entrySet()) {
        // exclude client parameters already covered by other runtime parameters that have been
        // added to clientEnv
        if (entry.getKey().equals(SFSessionProperty.APP_ID)
            || entry.getKey().equals(SFSessionProperty.APP_VERSION)) {
          continue;
        }
        String propKey = entry.getKey().getPropertyKey();
        // mask sensitive values like passwords, tokens, etc
        String propVal = SecretDetector.maskParameterValue(propKey, entry.getValue().toString());
        clientEnv.put(propKey, propVal);
      }
      // if map does not contain the tracing property, the default is set. Add
      // this default value to the map.
      if (!connectionPropertiesMap.containsKey(SFSessionProperty.TRACING)) {
        clientEnv.put(SFSessionProperty.TRACING.getPropertyKey(), tracingLevel);
      }

      clientEnv.put("JDBC_JAR_NAME", SnowflakeDriver.getJdbcJarname());

      data.put(ClientAuthnParameter.CLIENT_ENVIRONMENT.name(), clientEnv);

      // Initialize the session parameters
      Map sessionParameter = loginInput.getSessionParameters();
      if (loginInput.isValidateDefaultParameters()) {
        sessionParameter.put(CLIENT_VALIDATE_DEFAULT_PARAMETERS, true);
      }

      if (sessionParameter != null) {
        data.put(ClientAuthnParameter.SESSION_PARAMETERS.name(), loginInput.getSessionParameters());
      }

      if (loginInput.getAccountName() != null) {
        data.put(ClientAuthnParameter.ACCOUNT_NAME.name(), loginInput.getAccountName());
      }

      // Second Factor Authentication
      if (loginInput.isPasscodeInPassword()) {
        data.put(ClientAuthnParameter.EXT_AUTHN_DUO_METHOD.name(), "passcode");
      } else if (loginInput.getPasscode() != null) {
        data.put(ClientAuthnParameter.EXT_AUTHN_DUO_METHOD.name(), "passcode");
        data.put(ClientAuthnParameter.PASSCODE.name(), loginInput.getPasscode());
      } else {
        data.put(ClientAuthnParameter.EXT_AUTHN_DUO_METHOD.name(), "push");
      }

      data.put(ClientAuthnParameter.CLIENT_APP_VERSION.name(), loginInput.getAppVersion());

      authnData.setData(data);
      String json = mapper.writeValueAsString(authnData);

      postRequest = new HttpPost(loginURI);

      // Add custom headers before adding common headers
      HttpUtil.applyAdditionalHeadersForSnowsight(
          postRequest, loginInput.getAdditionalHttpHeadersForSnowsight());

      // 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());

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

      postRequest.addHeader("accept", "application/json");
      postRequest.addHeader("Accept-Encoding", "");

      /*
       * HttpClient should take authorization header from char[] instead of
       * String.
       */
      postRequest.setHeader(SF_HEADER_AUTHORIZATION, SF_HEADER_BASIC_AUTHTYPE);

      setServiceNameHeader(loginInput, postRequest);

      String theString = null;

      int leftRetryTimeout = loginInput.getLoginTimeout();
      int leftsocketTimeout = loginInput.getSocketTimeout();
      int retryCount = 0;

      while (true) {
        try {
          theString =
              HttpUtil.executeGeneralRequest(
                  postRequest,
                  leftRetryTimeout,
                  loginInput.getAuthTimeout(),
                  leftsocketTimeout,
                  retryCount,
                  loginInput.getHttpClientSettingsKey());
        } catch (SnowflakeSQLException ex) {
          if (ex.getErrorCode() == ErrorCode.AUTHENTICATOR_REQUEST_TIMEOUT.getMessageCode()) {
            if (authenticatorType == ClientAuthnDTO.AuthenticatorType.SNOWFLAKE_JWT) {
              SessionUtilKeyPair s =
                  new SessionUtilKeyPair(
                      loginInput.getPrivateKey(),
                      loginInput.getPrivateKeyFile(),
                      loginInput.getPrivateKeyFilePwd(),
                      loginInput.getAccountName(),
                      loginInput.getUserName());

              data.put(ClientAuthnParameter.TOKEN.name(), s.issueJwtToken());

              long elapsedSeconds = ex.getElapsedSeconds();

              if (loginInput.getLoginTimeout() > 0) {
                if (leftRetryTimeout > elapsedSeconds) {
                  leftRetryTimeout -= elapsedSeconds;
                } else {
                  leftRetryTimeout = 1;
                }
              }

              // 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
              // the actual socket timeout from customer setting.
              if (loginInput.getSocketTimeout() > 0) {
                if (ex.issocketTimeoutNoBackoff()) {
                  if (leftsocketTimeout > elapsedSeconds) {
                    leftsocketTimeout -= elapsedSeconds;
                  } else {
                    leftsocketTimeout = 1;
                  }
                } else {
                  // reset curl timeout for retry with backoff.
                  leftsocketTimeout = loginInput.getSocketTimeout();
                }
              }

              // JWT renew should not count as a retry, so we pass back the current retry count.
              retryCount = ex.getRetryCount();

              continue;
            }
          } else {
            throw ex;
          }
        }
        break;
      }

      // general method, same as with data binding
      JsonNode jsonNode = mapper.readTree(theString);

      // check the success field first
      if (!jsonNode.path("success").asBoolean()) {
        logger.debug("response = {}", theString);

        int errorCode = jsonNode.path("code").asInt();
        if (errorCode == Constants.ID_TOKEN_INVALID_LOGIN_REQUEST_GS_CODE) {
          // clean id_token first
          loginInput.setIdToken(null);
          deleteIdTokenCache(loginInput.getHostFromServerUrl(), loginInput.getUserName());

          logger.debug(
              "ID Token Expired / Not Applicable. Reauthenticating without ID Token...: {}",
              errorCode);
          SnowflakeUtil.checkErrorAndThrowExceptionIncludingReauth(jsonNode);
        }

        if (authenticatorType == ClientAuthnDTO.AuthenticatorType.USERNAME_PASSWORD_MFA) {
          deleteMfaTokenCache(loginInput.getHostFromServerUrl(), loginInput.getUserName());
        }

        throw new SnowflakeSQLException(
            NO_QUERY_ID,
            jsonNode.path("message").asText(),
            SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
            errorCode);
      }

      // session token is in the data field of the returned json response
      sessionToken = jsonNode.path("data").path("token").asText();
      masterToken = jsonNode.path("data").path("masterToken").asText();
      idToken = nullStringAsEmptyString(jsonNode.path("data").path("idToken").asText());
      mfaToken = nullStringAsEmptyString(jsonNode.path("data").path("mfaToken").asText());
      masterTokenValidityInSeconds = jsonNode.path("data").path("masterValidityInSeconds").asLong();
      String serverVersion = jsonNode.path("data").path("serverVersion").asText();
      sessionId = jsonNode.path("data").path("sessionId").asText();

      JsonNode dbNode = jsonNode.path("data").path("sessionInfo").path("databaseName");
      sessionDatabase = dbNode.isNull() ? null : dbNode.asText();
      JsonNode schemaNode = jsonNode.path("data").path("sessionInfo").path("schemaName");
      sessionSchema = schemaNode.isNull() ? null : schemaNode.asText();
      JsonNode roleNode = jsonNode.path("data").path("sessionInfo").path("roleName");
      sessionRole = roleNode.isNull() ? null : roleNode.asText();
      JsonNode warehouseNode = jsonNode.path("data").path("sessionInfo").path("warehouseName");
      sessionWarehouse = warehouseNode.isNull() ? null : warehouseNode.asText();

      commonParams = SessionUtil.getCommonParams(jsonNode.path("data").path("parameters"));

      if (serverVersion != null) {
        logger.debug("server version = {}", serverVersion);

        if (serverVersion.indexOf(" ") > 0) {
          databaseVersion = serverVersion.substring(0, serverVersion.indexOf(" "));
        } else {
          databaseVersion = serverVersion;
        }
      } else {
        logger.debug("server version is null", false);
      }

      if (databaseVersion != null) {
        String[] components = databaseVersion.split("\\.");
        if (components.length >= 2) {
          try {
            databaseMajorVersion = Integer.parseInt(components[0]);
            databaseMinorVersion = Integer.parseInt(components[1]);
          } catch (Exception ex) {
            logger.error(
                "Exception encountered when parsing server " + "version: {} Exception: {}",
                databaseVersion,
                ex.getMessage());
          }
        }
      } else {
        logger.debug("database version is null", false);
      }

      if (!jsonNode.path("data").path("newClientForUpgrade").isNull()) {
        newClientForUpgrade = jsonNode.path("data").path("newClientForUpgrade").asText();

        logger.debug("new client: {}", newClientForUpgrade);
      }

      // get health check interval and adjust network timeouts if different
      int healthCheckIntervalFromGS = jsonNode.path("data").path("healthCheckInterval").asInt();

      logger.debug("health check interval = {}", healthCheckIntervalFromGS);

      if (healthCheckIntervalFromGS > 0 && healthCheckIntervalFromGS != healthCheckInterval) {
        // add health check interval to socket timeout
        httpClientSocketTimeout =
            loginInput.getSocketTimeout() + (healthCheckIntervalFromGS * 1000);

        final RequestConfig requestConfig =
            RequestConfig.copy(HttpUtil.getRequestConfigWithoutCookies())
                .setConnectTimeout(loginInput.getConnectionTimeout())
                .setSocketTimeout(httpClientSocketTimeout)
                .build();

        HttpUtil.setRequestConfig(requestConfig);

        logger.debug("adjusted connection timeout to = {}", loginInput.getConnectionTimeout());

        logger.debug("adjusted socket timeout to = {}", httpClientSocketTimeout);
      }
    } catch (SnowflakeSQLException ex) {
      throw ex; // must catch here to avoid Throwable to get the exception
    } catch (IOException ex) {
      logger.error("IOException when creating session: " + postRequest, ex);

      throw new SnowflakeSQLException(
          ex,
          SqlState.IO_ERROR,
          ErrorCode.NETWORK_ERROR.getMessageCode(),
          "Exception encountered when opening connection: " + ex.getMessage());
    } catch (Throwable ex) {
      logger.error("Exception when creating session: " + postRequest, ex);

      throw new SnowflakeSQLException(
          ex,
          SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION,
          ErrorCode.CONNECTION_ERROR.getMessageCode(),
          ErrorCode.CONNECTION_ERROR.getMessageCode(),
          ex.getMessage());
    }

    SFLoginOutput ret =
        new SFLoginOutput(
            sessionToken,
            masterToken,
            masterTokenValidityInSeconds,
            idToken,
            mfaToken,
            databaseVersion,
            databaseMajorVersion,
            databaseMinorVersion,
            httpClientSocketTimeout,
            sessionDatabase,
            sessionSchema,
            sessionRole,
            sessionWarehouse,
            sessionId,
            commonParams);

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

    if (asBoolean(loginInput.getSessionParameters().get(CLIENT_REQUEST_MFA_TOKEN))) {
      CredentialManager.getInstance().writeMfaToken(loginInput, ret);
    }

    return ret;
  }

  private static void setServiceNameHeader(SFLoginInput loginInput, HttpPost postRequest) {
    if (!Strings.isNullOrEmpty(loginInput.getServiceName())) {
      // service name is used to route a request to appropriate cluster.
      postRequest.setHeader(SF_HEADER_SERVICE_NAME, loginInput.getServiceName());
    }
  }

  private static String nullStringAsEmptyString(String value) {
    if (Strings.isNullOrEmpty(value) || "null".equals(value)) {
      return "";
    }
    return value;
  }

  /** Delete the id token cache */
  public static void deleteIdTokenCache(String host, String user) {
    CredentialManager.getInstance().deleteIdTokenCache(host, user);
  }

  public static void deleteMfaTokenCache(String host, String user) {
    CredentialManager.getInstance().deleteMfaTokenCache(host, user);
  }

  /**
   * Renew a session.
   *
   * 

Use cases: - Session and Master tokens are provided. No Id token: - succeed in getting a new * Session token. - fail and raise SnowflakeReauthenticationRequest because Master token expires. * Since no id token exists, the exception is thrown to the upstream. - Session and Id tokens are * provided. No Master token: - fail and raise SnowflakeReauthenticationRequest and issue a new * Session token - fail and raise SnowflakeReauthenticationRequest and fail to issue a new Session * token as the * * @param loginInput login information * @return login output * @throws SFException if unexpected uri information * @throws SnowflakeSQLException if failed to renew the session */ static SFLoginOutput renewSession(SFLoginInput loginInput) throws SFException, SnowflakeSQLException { return tokenRequest(loginInput, TokenRequestType.RENEW); } private static SFLoginOutput tokenRequest(SFLoginInput loginInput, TokenRequestType requestType) throws SFException, SnowflakeSQLException { AssertUtil.assertTrue(loginInput.getServerUrl() != null, "missing server URL for tokenRequest"); AssertUtil.assertTrue( loginInput.getMasterToken() != null, "missing master token for tokenRequest"); AssertUtil.assertTrue( loginInput.getSessionToken() != null, "missing session token for tokenRequest"); AssertUtil.assertTrue( loginInput.getLoginTimeout() >= 0, "negative login timeout for tokenRequest"); // build URL for login request URIBuilder uriBuilder; HttpPost postRequest; String sessionToken; String masterToken; try { uriBuilder = new URIBuilder(loginInput.getServerUrl()); uriBuilder.setPath(SF_PATH_TOKEN_REQUEST); uriBuilder.addParameter(SFSession.SF_QUERY_REQUEST_ID, UUIDUtils.getUUID().toString()); postRequest = new HttpPost(uriBuilder.build()); // 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()); // Add custom headers before adding common headers HttpUtil.applyAdditionalHeadersForSnowsight( postRequest, loginInput.getAdditionalHttpHeadersForSnowsight()); } catch (URISyntaxException ex) { logger.error("Exception when creating http request", ex); throw new SFException(ex, ErrorCode.INTERNAL_ERROR, "unexpected URI syntax exception:3"); } try { // input json with old session token and request type, notice the // session token needs to be quoted. Map payload = new HashMap<>(); String headerToken = loginInput.getMasterToken(); payload.put("oldSessionToken", loginInput.getSessionToken()); payload.put("requestType", requestType.value); String json = mapper.writeValueAsString(payload); // attach the login info json body to the post request StringEntity input = new StringEntity(json, StandardCharsets.UTF_8); input.setContentType("application/json"); postRequest.setEntity(input); postRequest.addHeader("accept", "application/json"); postRequest.setHeader( SF_HEADER_AUTHORIZATION, SF_HEADER_SNOWFLAKE_AUTHTYPE + " " + SF_HEADER_TOKEN_TAG + "=\"" + headerToken + "\""); setServiceNameHeader(loginInput, postRequest); logger.debug( "request type: {}, old session token: {}, " + "master token: {}", requestType.value, (ArgSupplier) () -> loginInput.getSessionToken() != null ? "******" : null, (ArgSupplier) () -> loginInput.getMasterToken() != null ? "******" : null); String theString = HttpUtil.executeGeneralRequest( postRequest, loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeout(), 0, loginInput.getHttpClientSettingsKey()); // general method, same as with data binding JsonNode jsonNode = mapper.readTree(theString); // check the success field first if (!jsonNode.path("success").asBoolean()) { logger.debug("response = {}", theString); String errorCode = jsonNode.path("code").asText(); String message = jsonNode.path("message").asText(); EventUtil.triggerBasicEvent( Event.EventType.NETWORK_ERROR, "SessionUtil:renewSession failure, error code=" + errorCode + ", message=" + message, true); SnowflakeUtil.checkErrorAndThrowExceptionIncludingReauth(jsonNode); } // session token is in the data field of the returned json response sessionToken = jsonNode.path("data").path("sessionToken").asText(); masterToken = jsonNode.path("data").path("masterToken").asText(); } catch (IOException ex) { logger.error("IOException when renewing session: " + postRequest, ex); // Any EventType.NETWORK_ERRORs should have been triggered before // exception was thrown. throw new SFException(ex, ErrorCode.NETWORK_ERROR, ex.getMessage()); } SFLoginOutput loginOutput = new SFLoginOutput(); loginOutput.setSessionToken(sessionToken).setMasterToken(masterToken); return loginOutput; } /** * Close a session * * @param loginInput login information * @throws SnowflakeSQLException if failed to close session * @throws SFException if failed to close session */ static void closeSession(SFLoginInput loginInput) throws SFException, SnowflakeSQLException { logger.debug(" public void close() throws SFException"); // assert the following inputs are valid AssertUtil.assertTrue( loginInput.getServerUrl() != null, "missing server URL for closing session"); AssertUtil.assertTrue( loginInput.getSessionToken() != null, "missing session token for closing session"); AssertUtil.assertTrue( loginInput.getLoginTimeout() >= 0, "missing login timeout for closing session"); HttpPost postRequest = null; try { URIBuilder uriBuilder; uriBuilder = new URIBuilder(loginInput.getServerUrl()); uriBuilder.addParameter(SF_QUERY_SESSION_DELETE, Boolean.TRUE.toString()); uriBuilder.addParameter(SFSession.SF_QUERY_REQUEST_ID, UUIDUtils.getUUID().toString()); uriBuilder.setPath(SF_PATH_SESSION); postRequest = new HttpPost(uriBuilder.build()); // Add custom headers before adding common headers HttpUtil.applyAdditionalHeadersForSnowsight( postRequest, loginInput.getAdditionalHttpHeadersForSnowsight()); postRequest.setHeader( SF_HEADER_AUTHORIZATION, SF_HEADER_SNOWFLAKE_AUTHTYPE + " " + SF_HEADER_TOKEN_TAG + "=\"" + loginInput.getSessionToken() + "\""); setServiceNameHeader(loginInput, postRequest); String theString = HttpUtil.executeGeneralRequest( postRequest, loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeout(), 0, loginInput.getHttpClientSettingsKey()); JsonNode rootNode; logger.debug("connection close response: {}", theString); rootNode = mapper.readTree(theString); SnowflakeUtil.checkErrorAndThrowException(rootNode); } catch (URISyntaxException ex) { throw new RuntimeException("unexpected URI syntax exception", ex); } catch (IOException ex) { logger.error("unexpected IO exception for: " + postRequest, ex); } catch (SnowflakeSQLException ex) { // ignore exceptions for session expiration exceptions and for // sessions that no longer exist if (ex.getErrorCode() != Constants.SESSION_EXPIRED_GS_CODE && ex.getErrorCode() != Constants.SESSION_GONE) { throw ex; } } } /** * Given access token, query IDP URL snowflake app to get SAML response We also need to perform * important client side validation: validate the post back url come back with the SAML response * contains the same prefix as the Snowflake's server url, which is the intended destination url * to Snowflake. Explanation: This emulates the behavior of IDP initiated login flow in the user * browser where the IDP instructs the browser to POST the SAML assertion to the specific SP * endpoint. This is critical in preventing a SAML assertion issued to one SP from being sent to * another SP. * * @param loginInput Login Info for the request * @param ssoUrl URL to use for SSO * @param oneTimeToken The token used for SSO * @return The response in HTML form * @throws SnowflakeSQLException Will be thrown if the destination URL in the SAML assertion does * not match */ 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); HeaderGroup headers = new HeaderGroup(); headers.addHeader(new BasicHeader(HttpHeaders.ACCEPT, "*/*")); httpGet.setHeaders(headers.getAllHeaders()); responseHtml = HttpUtil.executeGeneralRequest( httpGet, loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeout(), 0, loginInput.getHttpClientSettingsKey()); // step 5 String postBackUrl = getPostBackUrlFromHTML(responseHtml); if (!isPrefixEqual(postBackUrl, loginInput.getServerUrl())) { URL idpDestinationUrl = new URL(postBackUrl); URL clientDestinationUrl = new URL(loginInput.getServerUrl()); String idpDestinationHostName = idpDestinationUrl.getHost(); String clientDestinationHostName = clientDestinationUrl.getHost(); logger.error( "The Snowflake hostname specified in the client connection {} does not match " + "the destination hostname in the SAML response returned by the IdP: {}", clientDestinationHostName, idpDestinationHostName); // Session is in process of getting created, so exception constructor takes in null session // value throw new SnowflakeSQLLoggedException( null, ErrorCode.IDP_INCORRECT_DESTINATION.getMessageCode(), SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION /* session = */ ); } } catch (IOException | URISyntaxException ex) { handleFederatedFlowError(loginInput, ex); } return responseHtml; } /** * Query IDP token url to authenticate and retrieve access token * * @param loginInput The login info for the request * @param tokenUrl The URL used to retrieve the access token * @return Returns the one time token * @throws SnowflakeSQLException Will be thrown if the execute request fails */ private static String federatedFlowStep3(SFLoginInput loginInput, String tokenUrl) throws SnowflakeSQLException { String oneTimeToken = ""; try { 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()); final String idpResponse = HttpUtil.executeRequestWithoutCookies( postRequest, loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeout(), 0, 0, null, loginInput.getHttpClientSettingsKey()); logger.debug("user is authenticated against {}.", loginInput.getAuthenticator()); // session token is in the data field of the returned json response final JsonNode jsonNode = mapper.readTree(idpResponse); oneTimeToken = jsonNode.get("sessionToken") != null ? jsonNode.get("sessionToken").asText() : jsonNode.get("cookieToken").asText(); } catch (IOException | URISyntaxException ex) { handleFederatedFlowError(loginInput, ex); } return oneTimeToken; } /** * Perform important client side validation: validate both token url and sso url contains same * prefix (protocol + host + port) as the given authenticator url. Explanation: This provides a * way for the user to 'authenticate' the IDP it is sending his/her credentials to. Without such a * check, the user could be coerced to provide credentials to an IDP impersonator. * * @param loginInput The login info for the request * @param tokenUrl The token URL * @param ssoUrl The SSO URL * @throws SnowflakeSQLException Will be thrown if the prefix for the tokenUrl and ssoUrl do not * match */ private static void federatedFlowStep2(SFLoginInput loginInput, String tokenUrl, String ssoUrl) throws SnowflakeSQLException { try { if (!isPrefixEqual(loginInput.getAuthenticator(), tokenUrl) || !isPrefixEqual(loginInput.getAuthenticator(), ssoUrl)) { logger.debug( "The specified authenticator {} is not supported.", loginInput.getAuthenticator()); // Session is in process of getting created, so exception constructor takes in null session // value throw new SnowflakeSQLLoggedException( null, ErrorCode.IDP_CONNECTION_ERROR.getMessageCode(), SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION /* session = */ ); } } catch (MalformedURLException ex) { handleFederatedFlowError(loginInput, ex); } } /** * Query Snowflake to obtain IDP token url and IDP SSO url * * @param loginInput The login info for the request * @throws SnowflakeSQLException Will be thrown if the execute request step fails */ 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 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(); authnData.setData(data); 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()); final String gsResponse = HttpUtil.executeGeneralRequest( postRequest, loginInput.getLoginTimeout(), loginInput.getAuthTimeout(), loginInput.getSocketTimeout(), 0, loginInput.getHttpClientSettingsKey()); logger.debug("authenticator-request response: {}", gsResponse); JsonNode jsonNode = mapper.readTree(gsResponse); // check the success field first if (!jsonNode.path("success").asBoolean()) { logger.debug("response = {}", gsResponse); int errorCode = jsonNode.path("code").asInt(); throw new SnowflakeSQLException( NO_QUERY_ID, jsonNode.path("message").asText(), SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION, errorCode); } // session token is in the data field of the returned json response dataNode = jsonNode.path("data"); } catch (IOException | URISyntaxException ex) { handleFederatedFlowError(loginInput, ex); } return dataNode; } /** * Logs an error generated during the federated authentication flow and re-throws it as a * SnowflakeSQLException. Note that we separate IOExceptions since those tend to be network * related. * * @param loginInput The login info from the request * @param ex The exception to process * @throws SnowflakeSQLException Will be thrown for all calls to this method */ private static void handleFederatedFlowError(SFLoginInput loginInput, Exception ex) throws SnowflakeSQLException { if (ex instanceof IOException) { logger.error("IOException when authenticating with " + loginInput.getAuthenticator(), ex); throw new SnowflakeSQLException( ex, SqlState.IO_ERROR, ErrorCode.NETWORK_ERROR.getMessageCode(), "Exception encountered when opening connection: " + ex.getMessage()); } logger.error("Exception when authenticating with " + loginInput.getAuthenticator(), ex); throw new SnowflakeSQLException( ex, SqlState.SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION, ErrorCode.CONNECTION_ERROR.getMessageCode(), ErrorCode.CONNECTION_ERROR.getMessageCode(), ex.getMessage()); } /** * FEDERATED FLOW See SNOW-27798 for additional details. * * @param loginInput The login info from the request * @return saml response * @throws SnowflakeSQLException Will be thrown if any of the federated steps fail */ private static String getSamlResponseUsingOkta(SFLoginInput loginInput) throws SnowflakeSQLException { JsonNode dataNode = federatedFlowStep1(loginInput); String tokenUrl = dataNode.path("tokenUrl").asText(); String ssoUrl = dataNode.path("ssoUrl").asText(); federatedFlowStep2(loginInput, tokenUrl, ssoUrl); final String oneTimeToken = federatedFlowStep3(loginInput, tokenUrl); return federatedFlowStep4(loginInput, ssoUrl, oneTimeToken); } /** * Verify if two input urls have the same protocol, host, and port. * * @param aUrlStr a source URL string * @param bUrlStr a target URL string * @return true if matched otherwise false * @throws MalformedURLException raises if a URL string is not valid. */ static boolean isPrefixEqual(String aUrlStr, String bUrlStr) throws MalformedURLException { URL aUrl = new URL(aUrlStr); URL bUrl = new URL(bUrlStr); int aPort = aUrl.getPort(); int bPort = bUrl.getPort(); if (aPort == -1 && "https".equals(aUrl.getProtocol())) { // default port number for HTTPS aPort = 443; } if (bPort == -1 && "https".equals(bUrl.getProtocol())) { // default port number for HTTPS bPort = 443; } // no default port number for HTTP is supported. return aUrl.getHost().equalsIgnoreCase(bUrl.getHost()) && aUrl.getProtocol().equalsIgnoreCase(bUrl.getProtocol()) && aPort == bPort; } /** * Extracts post back url from the HTML returned by the IDP * * @param html The HTML that we are parsing to find the post back url * @return The post back url */ private static String getPostBackUrlFromHTML(String html) { Document doc = Jsoup.parse(html); Elements e1 = doc.getElementsByTag("body"); Elements e2 = e1.get(0).getElementsByTag("form"); return e2.first().attr("action"); } /** * Helper function to parse a JsonNode from a GS response containing CommonParameters, emitting an * EnumMap of parameters * * @param paramsNode parameters in JSON form * @return map object including key and value pairs */ public static Map getCommonParams(JsonNode paramsNode) { Map parameters = new HashMap<>(); for (JsonNode child : paramsNode) { // If there isn't a name then the response from GS must be erroneous. if (!child.hasNonNull("name")) { logger.error("Common Parameter JsonNode encountered with " + "no parameter name!", false); continue; } // Look up the parameter based on the "name" attribute of the node. String paramName = child.path("name").asText(); // What type of value is it and what's the value? if (!child.hasNonNull("value")) { logger.debug("No value found for Common Parameter {}", child.path("name").asText()); continue; } if (STRING_PARAMS.contains(paramName.toUpperCase())) { parameters.put(paramName, child.path("value").asText()); } else if (INT_PARAMS.contains(paramName.toUpperCase())) { parameters.put(paramName, child.path("value").asInt()); } else if (BOOLEAN_PARAMS.contains(paramName.toUpperCase())) { parameters.put(paramName, child.path("value").asBoolean()); } else { try { // Value should only be boolean, int or string so we don't expect exceptions here. parameters.put(paramName, mapper.treeToValue(child.path("value"), Object.class)); } catch (Exception e) { logger.debug( "Unknown Common Parameter Failed to Parse: {} -> {}. Exception: {}", paramName, child.path("value"), e.getMessage()); } logger.debug("Unknown Common Parameter: {}", paramName); } logger.debug("Parameter {}: {}", paramName, child.path("value").asText()); } return parameters; } static void updateSfDriverParamValues(Map parameters, SFBaseSession session) { if (parameters != null && !parameters.isEmpty()) { session.setCommonParameters(parameters); } for (Map.Entry entry : parameters.entrySet()) { logger.debug("processing parameter {}", entry.getKey()); if ("CLIENT_DISABLE_INCIDENTS".equalsIgnoreCase(entry.getKey())) { SnowflakeDriver.setDisableIncidents((Boolean) entry.getValue()); } else if ("CLIENT_SESSION_KEEP_ALIVE".equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setEnableHeartbeat((Boolean) entry.getValue()); } } else if (CLIENT_SESSION_KEEP_ALIVE_HEARTBEAT_FREQUENCY.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setHeartbeatFrequency((int) entry.getValue()); } } else if ("CLIENT_ENABLE_LOG_INFO_STATEMENT_PARAMETERS".equalsIgnoreCase(entry.getKey())) { boolean enableLogging = (Boolean) entry.getValue(); if (session != null && session.getPreparedStatementLogging() != enableLogging) { session.setPreparedStatementLogging(enableLogging); } } else if ("AUTOCOMMIT".equalsIgnoreCase(entry.getKey())) { boolean autoCommit = (Boolean) entry.getValue(); if (session != null && session.getAutoCommit() != autoCommit) { session.setAutoCommit(autoCommit); } } else if (JDBC_RS_COLUMN_CASE_INSENSITIVE.equalsIgnoreCase(entry.getKey()) || CLIENT_RESULT_COLUMN_CASE_INSENSITIVE.equalsIgnoreCase(entry.getKey())) { if (session != null && !session.isResultColumnCaseInsensitive()) { session.setResultColumnCaseInsensitive((boolean) entry.getValue()); } } else if (CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setMetadataRequestUseConnectionCtx((boolean) entry.getValue()); } } else if (CLIENT_METADATA_USE_SESSION_DATABASE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setMetadataRequestUseSessionDatabase((boolean) entry.getValue()); } } else if (JDBC_TREAT_TIMESTAMP_NTZ_AS_UTC.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setTreatNTZAsUTC((boolean) entry.getValue()); } } else if (JDBC_FORMAT_DATE_WITH_TIMEZONE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setFormatDateWithTimezone((boolean) entry.getValue()); } } else if (JDBC_USE_SESSION_TIMEZONE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setUseSessionTimezone((boolean) entry.getValue()); } } else if ("CLIENT_TIMESTAMP_TYPE_MAPPING".equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setTimestampMappedType( SnowflakeType.valueOf(((String) entry.getValue()).toUpperCase())); } } else if ("JDBC_TREAT_DECIMAL_AS_INT".equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setJdbcTreatDecimalAsInt((boolean) entry.getValue()); } } else if ("JDBC_ENABLE_COMBINED_DESCRIBE".equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setEnableCombineDescribe((boolean) entry.getValue()); } } else if (CLIENT_IN_BAND_TELEMETRY_ENABLED.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setClientTelemetryEnabled((boolean) entry.getValue()); } } else if ("CLIENT_STAGE_ARRAY_BINDING_THRESHOLD".equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setArrayBindStageThreshold((int) entry.getValue()); } } else if (CLIENT_STORE_TEMPORARY_CREDENTIAL.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setStoreTemporaryCredential((boolean) entry.getValue()); } } else if (SERVICE_NAME.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setServiceName((String) entry.getValue()); } } else if (CLIENT_ENABLE_CONSERVATIVE_MEMORY_USAGE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setEnableConservativeMemoryUsage((boolean) entry.getValue()); } } else if (CLIENT_CONSERVATIVE_MEMORY_ADJUST_STEP.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setConservativeMemoryAdjustStep((int) entry.getValue()); } } else if (CLIENT_MEMORY_LIMIT.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setClientMemoryLimit((int) entry.getValue()); } } else if (CLIENT_RESULT_CHUNK_SIZE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setClientResultChunkSize((int) entry.getValue()); } } else if (CLIENT_PREFETCH_THREADS.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setClientPrefetchThreads((int) entry.getValue()); } } else if (CLIENT_OUT_OF_BAND_TELEMETRY_ENABLED.equalsIgnoreCase(entry.getKey())) { if ((boolean) entry.getValue()) { TelemetryService.enable(); } else { TelemetryService.disable(); } } else if (CLIENT_VALIDATE_DEFAULT_PARAMETERS.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setValidateDefaultParameters(SFLoginInput.getBooleanValue(entry.getValue())); } } else if (ENABLE_STAGE_S3_PRIVATELINK_FOR_US_EAST_1.equalsIgnoreCase((entry.getKey()))) { if (session != null) { session.setUseRegionalS3EndpointsForPresignedURL( SFLoginInput.getBooleanValue(entry.getValue())); } } else if (QUERY_CONTEXT_CACHE_SIZE.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setQueryContextCacheSize((int) entry.getValue()); } } else if (JDBC_ENABLE_PUT_GET.equalsIgnoreCase(entry.getKey())) { if (session != null) { session.setJdbcEnablePutGet(SFLoginInput.getBooleanValue(entry.getValue())); } } else { if (session != null) { session.setOtherParameter(entry.getKey(), entry.getValue()); } } } } enum TokenRequestType { RENEW("RENEW"), CLONE("CLONE"), ISSUE("ISSUE"); private String value; TokenRequestType(String value) { this.value = value; } } /** * Reset OCSP cache server if the snowflake server URL is for private link. If the URL is not for * private link, do nothing. * * @param serverUrl The Snowflake URL includes protocol such as "https://" */ public static void resetOCSPUrlIfNecessary(String serverUrl) throws IOException { if (serverUrl.indexOf(".privatelink.snowflakecomputing.com") > 0) { // Privatelink uses special OCSP Cache server URL url = new URL(serverUrl); String host = url.getHost(); logger.debug("HOST: {}", host); String ocspCacheServerUrl = String.format("http://ocsp.%s/%s", host, SFTrustManager.CACHE_FILE_NAME); logger.debug("OCSP Cache Server for Privatelink: {}", ocspCacheServerUrl); resetOCSPResponseCacherServerURL(ocspCacheServerUrl); } } /** * Helper function to generate a JWT token * * @param privateKey private key * @param privateKeyFile path to private key file * @param privateKeyFilePwd password for private key file * @param accountName account name * @param userName user name * @return JWT token * @throws SFException if Snowflake error occurs */ public static String generateJWTToken( PrivateKey privateKey, String privateKeyFile, String privateKeyFilePwd, String accountName, String userName) throws SFException { SessionUtilKeyPair s = new SessionUtilKeyPair( privateKey, privateKeyFile, privateKeyFilePwd, accountName, userName); return s.issueJwtToken(); } /** * Helper method to check if the request path is a login/auth request to use for retry strategy. * * @param request the post request * @return true if this is a login/auth request, false otherwise */ public static boolean isNewRetryStrategyRequest(HttpRequestBase request) { URI requestURI = request.getURI(); String requestPath = requestURI.getPath(); if (requestPath != null) { if (requestPath.equals(SF_PATH_LOGIN_REQUEST) || requestPath.equals(SF_PATH_AUTHENTICATOR_REQUEST) || requestPath.equals(SF_PATH_TOKEN_REQUEST)) { return true; } } return false; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy