From 538468afe4fd04352b36936d38c8c3b4a3509592 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 18 Oct 2018 16:49:51 -0700 Subject: [PATCH 01/22] Feature: Active Directory MSI Authentication Support --- .../microsoft/sqlserver/jdbc/IOBuffer.java | 1 + .../sqlserver/jdbc/SQLServerADAL4JUtils.java | 12 +- .../sqlserver/jdbc/SQLServerConnection.java | 124 ++++++++++++------ .../sqlserver/jdbc/SQLServerDriver.java | 5 +- .../jdbc/SQLServerPooledConnection.java | 6 - .../sqlserver/jdbc/SQLServerResource.java | 2 + .../sqlserver/jdbc/SqlFedAuthToken.java | 31 ----- .../com/microsoft/sqlserver/jdbc/Util.java | 30 ----- .../microsoft/sqlserver/jdbc/tdsparser.java | 2 +- 9 files changed, 99 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 0cabb2936..46c235090 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -108,6 +108,7 @@ final class TDS { static final int TDS_FEDAUTH_LIBRARY_RESERVED = 0x7F; static final byte ADALWORKFLOW_ACTIVEDIRECTORYPASSWORD = 0x01; static final byte ADALWORKFLOW_ACTIVEDIRECTORYINTEGRATED = 0x02; + static final byte ADALWORKFLOW_ACTIVEDIRECTORYMSI = 0x03; static final byte FEDAUTH_INFO_ID_STSURL = 0x01; // FedAuthInfoData is token endpoint URL from which to acquire fed // auth token static final byte FEDAUTH_INFO_ID_SPN = 0x02; // FedAuthInfoData is the SPN to use for acquiring fed auth token diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java index f0418a60c..e79f53455 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java @@ -28,7 +28,7 @@ class SQLServerADAL4JUtils { static final private java.util.logging.Logger adal4jLogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerADAL4JUtils"); - static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password, + static String getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newFixedThreadPool(1); try { @@ -37,10 +37,8 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String use ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID, user, password, null); AuthenticationResult authenticationResult = future.get(); - SqlFedAuthToken fedAuthToken = new SqlFedAuthToken(authenticationResult.getAccessToken(), - authenticationResult.getExpiresOnDate()); - return fedAuthToken; + return authenticationResult.getAccessToken(); } catch (MalformedURLException | InterruptedException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { @@ -64,7 +62,7 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String use } } - static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, + static String getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newFixedThreadPool(1); @@ -83,10 +81,8 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID, username, null, null); AuthenticationResult authenticationResult = future.get(); - SqlFedAuthToken fedAuthToken = new SqlFedAuthToken(authenticationResult.getAccessToken(), - authenticationResult.getExpiresOnDate()); - return fedAuthToken; + return authenticationResult.getAccessToken(); } catch (InterruptedException | IOException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 0ba3f48d3..c4e107cf9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -6,12 +6,18 @@ package com.microsoft.sqlserver.jdbc; import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.DatagramPacket; import java.net.DatagramSocket; +import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.SocketException; +import java.net.URL; import java.net.UnknownHostException; import java.sql.CallableStatement; import java.sql.Connection; @@ -123,8 +129,6 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private String authenticationString = null; private byte[] accessTokenInByte = null; - private SqlFedAuthToken fedAuthToken = null; - private String originalHostNameInCertificate = null; private Boolean isAzureDW = null; @@ -324,10 +328,6 @@ private static int[] locateParams(String sql) { return parameterPositions.stream().mapToInt(Integer::valueOf).toArray(); } - SqlFedAuthToken getAuthenticationResult() { - return fedAuthToken; - } - /** * Encapsulates the data to be sent to the server as part of Federated Authentication Feature Extension. */ @@ -349,6 +349,9 @@ class FederatedAuthenticationFeatureExtensionData { case "ACTIVEDIRECTORYINTEGRATED": this.authentication = SqlAuthentication.ActiveDirectoryIntegrated; break; + case "ACTIVEDIRECTORYMSI": + this.authentication = SqlAuthentication.ActiveDirectoryMSI; + break; default: assert (false); MessageFormat form = new MessageFormat( @@ -378,7 +381,9 @@ public String toString() { class ActiveDirectoryAuthentication { static final String JDBC_FEDAUTH_CLIENT_ID = "7f98cb04-cd1e-40df-9140-3bf7e2cea4db"; + static final String AZURE_REST_MSI_URL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"; static final String ADAL_GET_ACCESS_TOKEN_FUNCTION_NAME = "ADALGetAccessToken"; + static final String ACCESS_TOKEN_IDENTIFIER = "\"access_token\":\""; static final int GET_ACCESS_TOKEN_SUCCESS = 0; static final int GET_ACCESS_TOKEN_INVALID_GRANT = 1; static final int GET_ACCESS_TOKEN_TANSISENT_ERROR = 2; @@ -1051,12 +1056,6 @@ void checkClosed() throws SQLServerException { SQLServerException.makeFromDriverError(null, null, SQLServerException.getErrString("R_connectionIsClosed"), null, false); } - - if (null != fedAuthToken) { - if (Util.checkIfNeedNewAccessToken(this)) { - connect(this.activeConnectionProperties, null); - } - } } /** @@ -1565,6 +1564,19 @@ Connection connectInternal(Properties propsIn, null); } + if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()) + && ((!activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()) + .isEmpty()) + || (!activeConnectionProperties + .getProperty(SQLServerDriverStringProperty.PASSWORD.toString()).isEmpty()))) { + if (connectionlogger.isLoggable(Level.SEVERE)) { + connectionlogger.severe( + toString() + " " + SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword")); + } + throw new SQLServerException(SQLServerException.getErrString("R_MSIAuthenticationWithUserPassword"), + null); + } + if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString()) && ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()) .isEmpty()) @@ -3490,6 +3502,9 @@ int writeFedAuthFeatureRequest(boolean write, TDSWriter tdsWriter, case ActiveDirectoryIntegrated: workflow = TDS.ADALWORKFLOW_ACTIVEDIRECTORYINTEGRATED; break; + case ActiveDirectoryMSI: + workflow = TDS.ADALWORKFLOW_ACTIVEDIRECTORYMSI; + break; default: assert (false); // Unrecognized Authentication type for fedauth ADAL request break; @@ -3563,7 +3578,10 @@ private void logon(LogonCommand command) throws SQLServerException { // Extension // in Login7, indicating the intent to use Active Directory Authentication Library for SQL Server. if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString()) - || (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + || ((authenticationString.trim() + .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + || authenticationString.trim() + .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) && fedAuthRequiredPreLoginResponse)) { federatedAuthenticationInfoRequested = true; fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData(TDS.TDS_FEDAUTH_LIBRARY_ADAL, @@ -3982,16 +4000,16 @@ final void processFedAuthInfo(TDSReader tdsReader, TDSTokenHandler tdsTokenHandl final class FedAuthTokenCommand extends UninterruptableTDSCommand { TDSTokenHandler tdsTokenHandler = null; - SqlFedAuthToken fedAuthToken = null; + String accessToken = null; - FedAuthTokenCommand(SqlFedAuthToken fedAuthToken, TDSTokenHandler tdsTokenHandler) { + FedAuthTokenCommand(String accessToken, TDSTokenHandler tdsTokenHandler) { super("FedAuth"); this.tdsTokenHandler = tdsTokenHandler; - this.fedAuthToken = fedAuthToken; + this.accessToken = accessToken; } final boolean doExecute() throws SQLServerException { - sendFedAuthToken(this, fedAuthToken, tdsTokenHandler); + sendFedAuthToken(this, accessToken, tdsTokenHandler); return true; } } @@ -4003,23 +4021,24 @@ final boolean doExecute() throws SQLServerException { void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) throws SQLServerException { assert (null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()) && null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())) - || ((authenticationString.trim().equalsIgnoreCase( - SqlAuthentication.ActiveDirectoryIntegrated.toString()) && fedAuthRequiredPreLoginResponse)); + || (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + || authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()) + && fedAuthRequiredPreLoginResponse); assert null != fedAuthInfo; attemptRefreshTokenLocked = true; - fedAuthToken = getFedAuthToken(fedAuthInfo); + String accessToken = getFedAuthToken(fedAuthInfo); attemptRefreshTokenLocked = false; // fedAuthToken cannot be null. - assert null != fedAuthToken; + assert null != accessToken; - TDSCommand fedAuthCommand = new FedAuthTokenCommand(fedAuthToken, tdsTokenHandler); + TDSCommand fedAuthCommand = new FedAuthTokenCommand(accessToken, tdsTokenHandler); fedAuthCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(fedAuthCommand)); } - private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { - SqlFedAuthToken fedAuthToken = null; + private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { + String accessToken = null; // fedAuthInfo should not be null. assert null != fedAuthInfo; @@ -4032,11 +4051,14 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe while (true) { if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { - fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, + accessToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); // Break out of the retry loop in successful case. break; + } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { + accessToken = getMSIFedAuthToken(fedAuthInfo, authenticationString); + break; } else if (authenticationString.trim() .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { @@ -4054,9 +4076,7 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe byte[] accessTokenFromDLL = dllInfo.accessTokenBytes; - String accessToken = new String(accessTokenFromDLL, UTF_16LE); - - fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn); + accessToken = new String(accessTokenFromDLL, UTF_16LE); // Break out of the retry loop in successful case. break; @@ -4115,23 +4135,53 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe // so we don't need to check the // OS version here. else { - fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); + accessToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); } // Break out of the retry loop in successful case. break; } } - return fedAuthToken; + return accessToken; + } + + private String getMSIFedAuthToken(SqlFedAuthInfo fedAuthInfo, + String authenticationString) throws SQLServerException { + String urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + fedAuthInfo.spn; + HttpURLConnection connection = null; + + try { + connection = (HttpURLConnection) new URL(urlString).openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Metadata", "true"); + System.out.println("Attempting to get token with URL " + urlString); + connection.connect(); + + try (InputStream stream = connection.getInputStream()) { + + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8), 100); + String result = reader.readLine(); + int startIndex = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER.length(); + + return result.substring(startIndex, result.indexOf("\"", startIndex + 1)); + } + } catch (Exception e) { + SQLServerException.makeFromDriverError(this, null, e.getMessage(), null, true); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } } /** * Send the access token to the server. */ - private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, SqlFedAuthToken fedAuthToken, + private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, String accessToken, TDSTokenHandler tdsTokenHandler) throws SQLServerException { - assert null != fedAuthToken; - assert null != fedAuthToken.accessToken; + assert null != accessToken; if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.fine(toString() + " Sending federated authentication token."); @@ -4139,17 +4189,17 @@ private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, SqlFedAuthToke TDSWriter tdsWriter = fedAuthCommand.startRequest(TDS.PKT_FEDAUTH_TOKEN_MESSAGE); - byte[] accessToken = fedAuthToken.accessToken.getBytes(UTF_16LE); + byte[] accessTokenBytes = accessToken.getBytes(UTF_16LE); // Send total length (length of token plus 4 bytes for the token length field) // If we were sending a nonce, this would include that length as well - tdsWriter.writeInt(accessToken.length + 4); + tdsWriter.writeInt(accessTokenBytes.length + 4); // Send length of token - tdsWriter.writeInt(accessToken.length); + tdsWriter.writeInt(accessTokenBytes.length); // Send federated authentication access token. - tdsWriter.writeBytes(accessToken, 0, accessToken.length); + tdsWriter.writeBytes(accessTokenBytes, 0, accessTokenBytes.length); TDSReader tdsReader; tdsReader = fedAuthCommand.startResponse(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 30a0c4d76..70d8328b7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -65,7 +65,8 @@ enum SqlAuthentication { NotSpecified, SqlPassword, ActiveDirectoryPassword, - ActiveDirectoryIntegrated; + ActiveDirectoryIntegrated, + ActiveDirectoryMSI; static SqlAuthentication valueOfString(String value) throws SQLServerException { SqlAuthentication method = null; @@ -80,6 +81,8 @@ static SqlAuthentication valueOfString(String value) throws SQLServerException { } else if (value.toLowerCase(Locale.US) .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { method = SqlAuthentication.ActiveDirectoryIntegrated; + } else if (value.toLowerCase(Locale.US).equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { + method = SqlAuthentication.ActiveDirectoryMSI; } else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidConnectionSetting")); Object[] msgArgs = {"authentication", value}; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index 91b8a5144..fe63e4c0b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -94,12 +94,6 @@ public Connection getConnection() throws SQLException { if (pcLogger.isLoggable(Level.FINE)) pcLogger.fine(toString() + " Physical connection, " + safeCID()); - if (null != physicalConnection.getAuthenticationResult()) { - if (Util.checkIfNeedNewAccessToken(physicalConnection)) { - physicalConnection = createNewConnection(); - } - } - // The last proxy connection handle returned will be invalidated (moved to closed state) // when getConnection is called. if (null != lastProxyConnection) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 7f30ca791..d544c8e8a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -386,6 +386,8 @@ protected Object[][] getContents() { "Cannot set the AccessToken property if the \"IntegratedSecurity\" connection string keyword has been set to \"true\"."}, {"R_IntegratedAuthenticationWithUserPassword", "Cannot use \"Authentication=ActiveDirectoryIntegrated\" with \"User\", \"UserName\" or \"Password\" connection string keywords."}, + {"R_MSIAuthenticationWithUserPassword", + "Cannot use \"Authentication=ActiveDirectoryMSI\" with \"User\", \"UserName\" or \"Password\" connection string keywords."}, {"R_AccessTokenWithUserPassword", "Cannot set the AccessToken property if \"User\", \"UserName\" or \"Password\" has been specified in the connection string."}, {"R_AccessTokenCannotBeEmpty", "AccesToken cannot be empty."}, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java deleted file mode 100644 index 01adb70a1..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made - * available under the terms of the MIT License. See the LICENSE file in the project root for more information. - */ - -package com.microsoft.sqlserver.jdbc; - -import java.util.Date; - - -class SqlFedAuthToken { - final Date expiresOn; - final String accessToken; - - SqlFedAuthToken(final String accessToken, final long expiresIn) { - this.accessToken = accessToken; - - Date now = new Date(); - now.setTime(now.getTime() + (expiresIn * 1000)); - this.expiresOn = now; - } - - SqlFedAuthToken(final String accessToken, final Date expiresOn) { - this.accessToken = accessToken; - this.expiresOn = expiresOn; - } - - Date getExpiresOnDate() { - return expiresOn; - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index 087c2408b..3d189fe2c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -11,7 +11,6 @@ import java.net.UnknownHostException; import java.text.DecimalFormat; import java.text.MessageFormat; -import java.util.Date; import java.util.Locale; import java.util.Properties; import java.util.Set; @@ -932,35 +931,6 @@ else if (("" + value).contains("E")) { return 0; } - // If the access token is expiring within next 10 minutes, lets just re-create a token for this connection attempt. - // If the token is expiring within the next 45 mins, try to fetch a new token if there is no thread already doing - // it. - // If a thread is already doing the refresh, just use the existing token and proceed. - static synchronized boolean checkIfNeedNewAccessToken(SQLServerConnection connection) { - Date accessTokenExpireDate = connection.getAuthenticationResult().getExpiresOnDate(); - Date now = new Date(); - - // if the token's expiration is within the next 45 mins - // 45 mins * 60 sec/min * 1000 millisec/sec - if ((accessTokenExpireDate.getTime() - now.getTime()) < (45 * 60 * 1000)) { - - // within the next 10 mins - if ((accessTokenExpireDate.getTime() - now.getTime()) < (10 * 60 * 1000)) { - return true; - } else { - // check if another thread is already updating the access token - if (connection.attemptRefreshTokenLocked) { - return false; - } else { - connection.attemptRefreshTokenLocked = true; - return true; - } - } - } - - return false; - } - static final boolean use43Wrapper; static { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java index abfae2a83..c01f0f1b1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java @@ -41,7 +41,7 @@ static void parse(TDSReader tdsReader, TDSTokenHandler tdsTokenHandler) throws S logger.finest(tdsReader.toString() + ": " + tdsTokenHandler.logContext + ": Processing " + ((-1 == tdsTokenType) ? "EOF" : TDS.getTokenName(tdsTokenType))); } - + System.out.println(TDS.getTokenName(tdsTokenType)); switch (tdsTokenType) { case TDS.TDS_SSPI: parsing = tdsTokenHandler.onSSPI(tdsReader); From b5b3934f444517fcecdb30afd5b65210eb2c3b7c Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 18 Oct 2018 16:54:51 -0700 Subject: [PATCH 02/22] Remove Debug Logs --- .../java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java | 2 +- src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c4e107cf9..5768f1008 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4154,7 +4154,7 @@ private String getMSIFedAuthToken(SqlFedAuthInfo fedAuthInfo, connection = (HttpURLConnection) new URL(urlString).openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Metadata", "true"); - System.out.println("Attempting to get token with URL " + urlString); + connection.connect(); try (InputStream stream = connection.getInputStream()) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java index c01f0f1b1..abfae2a83 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java @@ -41,7 +41,7 @@ static void parse(TDSReader tdsReader, TDSTokenHandler tdsTokenHandler) throws S logger.finest(tdsReader.toString() + ": " + tdsTokenHandler.logContext + ": Processing " + ((-1 == tdsTokenType) ? "EOF" : TDS.getTokenName(tdsTokenType))); } - System.out.println(TDS.getTokenName(tdsTokenType)); + switch (tdsTokenType) { case TDS.TDS_SSPI: parsing = tdsTokenHandler.onSSPI(tdsReader); From d01293de1292076d3adb9ea1416d51a1d75c6b16 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 18 Oct 2018 20:54:15 -0700 Subject: [PATCH 03/22] Add support for User Assigned Managed Identity --- .../sqlserver/jdbc/ISQLServerDataSource.java | 15 +++++++++++ .../sqlserver/jdbc/SQLServerConnection.java | 27 ++++++++++++++----- .../sqlserver/jdbc/SQLServerDataSource.java | 12 +++++++++ .../sqlserver/jdbc/SQLServerDriver.java | 5 +++- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index a0aedb4a5..15cc915fe 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -805,4 +805,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * indicates whether Bulk Copy API should be used for Batch Insert operations. */ public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert); + + /** + * Sets the object id to be used to retrieve access token from MSI EndPoint. + * + * @param msiObjectId + * Object ID of User Assigned Managed Identity + */ + public void setMSIObjectId(String msiObjectId); + + /** + * Returns the value for the connection property 'msiObjectId'. + * + * @return msiObjectId property value + */ + public String getMSIObjectId(); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 5768f1008..ff9c263d2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1237,6 +1237,10 @@ Connection connectInternal(Properties propsIn, && !hostNameInCertificate.isEmpty()) { originalHostNameInCertificate = activeConnectionProperties .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString()); + } else { + String serverName = activeConnectionProperties + .getProperty(SQLServerDriverStringProperty.SERVER_NAME.toString()); + originalHostNameInCertificate = "*" + serverName.substring(serverName.indexOf('.')); } if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) { @@ -1852,6 +1856,12 @@ else if (0 == requestedPacketSize) activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString()); } + sPropKey = SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null != sPropValue) { + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } + FailoverInfo fo = null; String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString(); String serverNameProperty = SQLServerDriverStringProperty.SERVER_NAME.toString(); @@ -4057,7 +4067,10 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep // Break out of the retry loop in successful case. break; } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { - accessToken = getMSIFedAuthToken(fedAuthInfo, authenticationString); + accessToken = getMSIAuthToken(fedAuthInfo.spn, + activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString())); + + // Break out of the retry loop in successful case. break; } else if (authenticationString.trim() .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { @@ -4073,9 +4086,7 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep // AccessToken should not be null. assert null != dllInfo.accessTokenBytes; - byte[] accessTokenFromDLL = dllInfo.accessTokenBytes; - accessToken = new String(accessTokenFromDLL, UTF_16LE); // Break out of the retry loop in successful case. @@ -4145,9 +4156,13 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep return accessToken; } - private String getMSIFedAuthToken(SqlFedAuthInfo fedAuthInfo, - String authenticationString) throws SQLServerException { - String urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + fedAuthInfo.spn; + private String getMSIAuthToken(String resource, String objectId) throws SQLServerException { + String urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; + + if (null != objectId && !objectId.isEmpty()) { + urlString += "&object_id=" + objectId; + } + HttpURLConnection connection = null; try { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index a0b32b32e..89c99b1c2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -866,6 +866,18 @@ public String getJASSConfigurationName() { SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue()); } + @Override + public void setMSIObjectId(String msiObjectId) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), + msiObjectId); + } + + @Override + public String getMSIObjectId() { + return getStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), + SQLServerDriverStringProperty.MSI_OBJECT_ID.getDefaultValue()); + } + /** * Sets a property string value. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 70d8328b7..7d3ba8310 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -283,7 +283,8 @@ enum SQLServerDriverStringProperty { KEY_STORE_AUTHENTICATION("keyStoreAuthentication", ""), KEY_STORE_SECRET("keyStoreSecret", ""), KEY_STORE_LOCATION("keyStoreLocation", ""), - SSL_PROTOCOL("sslProtocol", SSLProtocol.TLS.toString()),; + SSL_PROTOCOL("sslProtocol", SSLProtocol.TLS.toString()), + MSI_OBJECT_ID("msiObjectId", ""),; private final String name; private final String defaultValue; @@ -503,6 +504,8 @@ public final class SQLServerDriver implements java.sql.Driver { SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), SSLProtocol.TLS_V11.toString(), SSLProtocol.TLS_V12.toString()}), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), + SQLServerDriverStringProperty.MSI_OBJECT_ID.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue()), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(), From bd5d7f9a9b9bd67a5c17750efd7893d68789ea77 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 18 Oct 2018 21:59:07 -0700 Subject: [PATCH 04/22] Add Resource Description --- .../java/com/microsoft/sqlserver/jdbc/SQLServerResource.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index d544c8e8a..d9d38a4b1 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -245,6 +245,7 @@ protected Object[][] getContents() { {"R_statementPoolingCacheSizePropertyDescription", "This setting specifies the size of the prepared statement cache for a connection. A value less than 1 means no cache."}, {"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."}, + {"R_msiObjectIdPropertyDescription", "Object Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."}, From 39605ae3e18d8015597316f74778f32189c5ac4a Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 18 Oct 2018 22:05:21 -0700 Subject: [PATCH 05/22] Add checks for exception cases. --- .../com/microsoft/sqlserver/jdbc/SQLServerConnection.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index ff9c263d2..b2ec2b6fe 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1240,7 +1240,9 @@ Connection connectInternal(Properties propsIn, } else { String serverName = activeConnectionProperties .getProperty(SQLServerDriverStringProperty.SERVER_NAME.toString()); - originalHostNameInCertificate = "*" + serverName.substring(serverName.indexOf('.')); + if(null != serverName && !serverName.isEmpty() && serverName.contains(".")) { + originalHostNameInCertificate = "*" + serverName.substring(serverName.indexOf('.')); + } } if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) { From 810c8a8fee78b3a137e1c3dbe87fab963906ea9d Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 19 Oct 2018 11:02:47 -0700 Subject: [PATCH 06/22] Remove HostNameInCertificate fix for separate tracking --- .../com/microsoft/sqlserver/jdbc/SQLServerConnection.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index b2ec2b6fe..3e2b45dc5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1237,12 +1237,6 @@ Connection connectInternal(Properties propsIn, && !hostNameInCertificate.isEmpty()) { originalHostNameInCertificate = activeConnectionProperties .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString()); - } else { - String serverName = activeConnectionProperties - .getProperty(SQLServerDriverStringProperty.SERVER_NAME.toString()); - if(null != serverName && !serverName.isEmpty() && serverName.contains(".")) { - originalHostNameInCertificate = "*" + serverName.substring(serverName.indexOf('.')); - } } if (null != originalHostNameInCertificate && !originalHostNameInCertificate.isEmpty()) { From 8ae6eb00033045055f09bb8457aff6056b6d9135 Mon Sep 17 00:00:00 2001 From: Cheena Malholtra Date: Tue, 23 Oct 2018 16:08:42 -0700 Subject: [PATCH 07/22] Fetch and store Expiry time with tokens --- .../sqlserver/jdbc/SQLServerADAL4JUtils.java | 8 +-- .../sqlserver/jdbc/SQLServerConnection.java | 53 ++++++++++++++----- .../sqlserver/jdbc/SqlFedAuthToken.java | 31 +++++++++++ 3 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java index e79f53455..1ffd0a1d9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java @@ -28,7 +28,7 @@ class SQLServerADAL4JUtils { static final private java.util.logging.Logger adal4jLogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerADAL4JUtils"); - static String getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password, + static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newFixedThreadPool(1); try { @@ -38,7 +38,7 @@ static String getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String AuthenticationResult authenticationResult = future.get(); - return authenticationResult.getAccessToken(); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); } catch (MalformedURLException | InterruptedException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { @@ -62,7 +62,7 @@ static String getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String } } - static String getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, + static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newFixedThreadPool(1); @@ -82,7 +82,7 @@ static String getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, AuthenticationResult authenticationResult = future.get(); - return authenticationResult.getAccessToken(); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); } catch (InterruptedException | IOException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 3e2b45dc5..e1e061d69 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -33,6 +33,8 @@ import java.sql.Statement; import java.text.MessageFormat; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedList; @@ -128,6 +130,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData = null; private String authenticationString = null; private byte[] accessTokenInByte = null; + private Date accessTokenExpiry = null; private String originalHostNameInCertificate = null; @@ -384,6 +387,7 @@ class ActiveDirectoryAuthentication { static final String AZURE_REST_MSI_URL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"; static final String ADAL_GET_ACCESS_TOKEN_FUNCTION_NAME = "ADALGetAccessToken"; static final String ACCESS_TOKEN_IDENTIFIER = "\"access_token\":\""; + static final String ACCESS_TOKEN_EXPIRY_IDENTIFIER = "\"expires_in\":\""; static final int GET_ACCESS_TOKEN_SUCCESS = 0; static final int GET_ACCESS_TOKEN_INVALID_GRANT = 1; static final int GET_ACCESS_TOKEN_TANSISENT_ERROR = 2; @@ -4032,9 +4036,7 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) && fedAuthRequiredPreLoginResponse); assert null != fedAuthInfo; - attemptRefreshTokenLocked = true; String accessToken = getFedAuthToken(fedAuthInfo); - attemptRefreshTokenLocked = false; // fedAuthToken cannot be null. assert null != accessToken; @@ -4044,7 +4046,7 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) } private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { - String accessToken = null; + SqlFedAuthToken authToken = null; // fedAuthInfo should not be null. assert null != fedAuthInfo; @@ -4057,13 +4059,12 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep while (true) { if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { - accessToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, - authenticationString); + authToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); // Break out of the retry loop in successful case. break; } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { - accessToken = getMSIAuthToken(fedAuthInfo.spn, + authToken = getMSIAuthToken(fedAuthInfo.spn, activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString())); // Break out of the retry loop in successful case. @@ -4083,7 +4084,9 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep // AccessToken should not be null. assert null != dllInfo.accessTokenBytes; byte[] accessTokenFromDLL = dllInfo.accessTokenBytes; - accessToken = new String(accessTokenFromDLL, UTF_16LE); + + String accessToken = new String(accessTokenFromDLL, UTF_16LE); + authToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn); // Break out of the retry loop in successful case. break; @@ -4142,17 +4145,18 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep // so we don't need to check the // OS version here. else { - accessToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); + authToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); } // Break out of the retry loop in successful case. break; } } - return accessToken; + accessTokenExpiry = authToken.expiresOn; + return authToken.accessToken; } - private String getMSIAuthToken(String resource, String objectId) throws SQLServerException { + private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws SQLServerException { String urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; if (null != objectId && !objectId.isEmpty()) { @@ -4172,10 +4176,19 @@ private String getMSIAuthToken(String resource, String objectId) throws SQLServe BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8), 100); String result = reader.readLine(); - int startIndex = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) + + int startIndex_AT = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) + ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER.length(); + int startIndex_ATX = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRY_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRY_IDENTIFIER.length(); + + String accessToken = result.substring(startIndex_AT, result.indexOf("\"", startIndex_AT + 1)); + String accessTokenExpiry = result.substring(startIndex_ATX, result.indexOf("\"", startIndex_ATX + 1)); - return result.substring(startIndex, result.indexOf("\"", startIndex + 1)); + Calendar cal = new Calendar.Builder().setInstant(new Date()).build(); + cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); + + return new SqlFedAuthToken(accessToken, cal.getTime()); } } catch (Exception e) { SQLServerException.makeFromDriverError(this, null, e.getMessage(), null, true); @@ -4187,6 +4200,22 @@ private String getMSIAuthToken(String resource, String objectId) throws SQLServe } } + /** + * Checks if access token is nearing expiry and a new token is required instead. + * + * @return true if Access Token will expire in next 10 mins or less. false if Access Token has more than 10 mins to + * expire. + */ + protected boolean isAccessTokenExpired() { + Calendar now = new Calendar.Builder().setInstant(new Date()).build(); + // Subtract 10 minutes to allow buffer time for reconnection + now.add(Calendar.MINUTE, -10); + if (accessTokenExpiry.before(now.getTime())) { + return true; + } + return false; + } + /** * Send the access token to the server. */ diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java new file mode 100644 index 000000000..3a46abe1f --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java @@ -0,0 +1,31 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.util.Date; + + +class SqlFedAuthToken { + final Date expiresOn; + final String accessToken; + + SqlFedAuthToken(final String accessToken, final long expiresIn) { + this.accessToken = accessToken; + + Date now = new Date(); + now.setTime(now.getTime() + (expiresIn * 1000)); + this.expiresOn = now; + } + + SqlFedAuthToken(final String accessToken, final Date expiresOn) { + this.accessToken = accessToken; + this.expiresOn = expiresOn; + } + + Date getExpiresOnDate() { + return expiresOn; + } +} \ No newline at end of file From bc021b64adf59a13bb4cc8fc0b15b85039e34b16 Mon Sep 17 00:00:00 2001 From: Cheena Malholtra Date: Tue, 30 Oct 2018 22:51:36 -0700 Subject: [PATCH 08/22] Changes to add back Refresh Token logic (with improvements) --- .../sqlserver/jdbc/AuthenticationJNI.java | 1 - .../sqlserver/jdbc/SQLServerADAL4JUtils.java | 40 +++++++++- .../sqlserver/jdbc/SQLServerConnection.java | 78 +++++++++++++++---- .../sqlserver/jdbc/SQLServerResource.java | 8 +- .../sqlserver/jdbc/SqlFedAuthToken.java | 19 +++-- .../com/microsoft/sqlserver/jdbc/Util.java | 29 +++++++ 6 files changed, 148 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java index 0364a3a78..bd4a04df9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/AuthenticationJNI.java @@ -59,7 +59,6 @@ static boolean isDllLoaded() { enabled = true; } catch (UnsatisfiedLinkError e) { temp = e; - authLogger.warning("Failed to load the sqljdbc_auth.dll cause : " + e.getMessage()); // This is not re-thrown on purpose - the constructor will terminate the properly with the appropriate error // string } finally { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java index 1ffd0a1d9..ddcad8c3d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java @@ -38,7 +38,8 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String use AuthenticationResult authenticationResult = future.get(); - return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate(), + authenticationResult.getRefreshToken()); } catch (MalformedURLException | InterruptedException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { @@ -82,7 +83,8 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, AuthenticationResult authenticationResult = future.get(); - return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate(), + authenticationResult.getRefreshToken()); } catch (InterruptedException | IOException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { @@ -111,4 +113,38 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, executorService.shutdown(); } } + + static SqlFedAuthToken aquireTokenFromRefreshToken(SqlFedAuthInfo fedAuthInfo, SqlFedAuthToken fedAuthToken, + String authenticationString) { + ExecutorService executorService = Executors.newFixedThreadPool(1); + try { + // principal name does not matter, what matters is the realm name + // it gets the username in principal_name@realm_name format + KerberosPrincipal kerberosPrincipal = new KerberosPrincipal("username"); + + if (adal4jLogger.isLoggable(Level.FINE)) { + adal4jLogger.fine(adal4jLogger.toString() + " realm name is:" + kerberosPrincipal.getRealm()); + } + + AuthenticationContext context = new AuthenticationContext(fedAuthInfo.stsurl, false, executorService); + Future future = context.acquireTokenByRefreshToken(fedAuthToken.refreshToken, + ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID, null); + + AuthenticationResult authenticationResult = future.get(); + fedAuthToken.updateAccessToken(authenticationResult.getAccessToken(), + authenticationResult.getExpiresOnDate()); + + return fedAuthToken; + } catch (InterruptedException | IOException | ExecutionException e) { + // We do not want to throw any exception here as we will try again to acquire a + // fresh access Token. In case of any exception, simply log message as INFO + if (adal4jLogger.isLoggable(Level.INFO)) { + adal4jLogger.info(adal4jLogger.toString() + " Failed to acquire access token using refresh Token"); + } + return null; + } finally { + executorService.shutdown(); + } + } + } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index e1e061d69..2e2b1267b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -130,10 +130,17 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private FederatedAuthenticationFeatureExtensionData fedAuthFeatureExtensionData = null; private String authenticationString = null; private byte[] accessTokenInByte = null; - private Date accessTokenExpiry = null; + + private SqlFedAuthToken fedAuthToken = null; + private SqlFedAuthInfo sqlFedAuthInfo = null; private String originalHostNameInCertificate = null; + + protected SqlFedAuthToken getAuthenticationResult() { + return fedAuthToken; + } + private Boolean isAzureDW = null; static class CityHash128Key implements java.io.Serializable { @@ -1060,6 +1067,34 @@ void checkClosed() throws SQLServerException { SQLServerException.makeFromDriverError(null, null, SQLServerException.getErrString("R_connectionIsClosed"), null, false); } + + // Check if federated Authentication is in use + if (null != fedAuthToken) { + // Check if access token is about to expire soon + if (Util.checkIfNeedNewAccessToken(this)) { + boolean needsReconnect = false; + // Check if no refreshToken was received + // Applies to cases where nativeDLL is in use + if (null != fedAuthToken.refreshToken) { + // Attempt to refresh access token using refresh token + fedAuthToken = SQLServerADAL4JUtils.aquireTokenFromRefreshToken(sqlFedAuthInfo, fedAuthToken, + authenticationString); + attemptRefreshTokenLocked = false; + // Check if refreshing Access token has failed, would need reconnect + if (null == fedAuthToken) { + needsReconnect = true; + } + } else { + // Reconnect is needed if Native DLL is in use by user application. + needsReconnect = true; + } + if (needsReconnect) { + // Try acquiring a fresh access token now. + connect(this.activeConnectionProperties, null); + } + } + } + } /** @@ -3865,7 +3900,7 @@ final void processEnvChange(TDSReader tdsReader) throws SQLServerException { } final void processFedAuthInfo(TDSReader tdsReader, TDSTokenHandler tdsTokenHandler) throws SQLServerException { - SqlFedAuthInfo sqlFedAuthInfo = new SqlFedAuthInfo(); + sqlFedAuthInfo = new SqlFedAuthInfo(); tdsReader.readUnsignedByte(); // token type, 0xEE @@ -4036,17 +4071,18 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) && fedAuthRequiredPreLoginResponse); assert null != fedAuthInfo; - String accessToken = getFedAuthToken(fedAuthInfo); + SqlFedAuthToken fedAuthToken = getFedAuthToken(fedAuthInfo); // fedAuthToken cannot be null. - assert null != accessToken; + assert null != fedAuthToken; + assert null != fedAuthToken.accessToken; - TDSCommand fedAuthCommand = new FedAuthTokenCommand(accessToken, tdsTokenHandler); + TDSCommand fedAuthCommand = new FedAuthTokenCommand(fedAuthToken.accessToken, tdsTokenHandler); fedAuthCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(fedAuthCommand)); } - private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { - SqlFedAuthToken authToken = null; + private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { + SqlFedAuthInfo sqlFedAuthInfo = new SqlFedAuthInfo(); // fedAuthInfo should not be null. assert null != fedAuthInfo; @@ -4059,12 +4095,13 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep while (true) { if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { - authToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); + validateAdalLibrary("R_ADALMissing"); + fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); // Break out of the retry loop in successful case. break; } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { - authToken = getMSIAuthToken(fedAuthInfo.spn, + fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString())); // Break out of the retry loop in successful case. @@ -4086,7 +4123,7 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep byte[] accessTokenFromDLL = dllInfo.accessTokenBytes; String accessToken = new String(accessTokenFromDLL, UTF_16LE); - authToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn); + fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn, null); // Break out of the retry loop in successful case. break; @@ -4145,15 +4182,26 @@ private String getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerExcep // so we don't need to check the // OS version here. else { - authToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); + //Check if ADAL4J library is available + validateAdalLibrary("R_DLLandADALMissing"); + fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); } // Break out of the retry loop in successful case. break; } } - accessTokenExpiry = authToken.expiresOn; - return authToken.accessToken; + return fedAuthToken; + } + + private void validateAdalLibrary(String errorMessage) throws SQLServerException { + try { + Class.forName("com.microsoft.aad.adal4j.AuthenticationContext"); + } catch (ClassNotFoundException e) { + // throw Exception for missing libraries + MessageFormat form = new MessageFormat(SQLServerException.getErrString(errorMessage)); + throw new SQLServerException(form.format(new Object[] {authenticationString.trim()}), null, 0, null); + } } private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws SQLServerException { @@ -4188,7 +4236,7 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws Calendar cal = new Calendar.Builder().setInstant(new Date()).build(); cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); - return new SqlFedAuthToken(accessToken, cal.getTime()); + return new SqlFedAuthToken(accessToken, cal.getTime(), null); } } catch (Exception e) { SQLServerException.makeFromDriverError(this, null, e.getMessage(), null, true); @@ -4210,7 +4258,7 @@ protected boolean isAccessTokenExpired() { Calendar now = new Calendar.Builder().setInstant(new Date()).build(); // Subtract 10 minutes to allow buffer time for reconnection now.add(Calendar.MINUTE, -10); - if (accessTokenExpiry.before(now.getTime())) { + if (fedAuthToken.expiresOn.before(now.getTime())) { return true; } return false; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index d9d38a4b1..1b1e4efa2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -245,7 +245,8 @@ protected Object[][] getContents() { {"R_statementPoolingCacheSizePropertyDescription", "This setting specifies the size of the prepared statement cache for a connection. A value less than 1 means no cache."}, {"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."}, - {"R_msiObjectIdPropertyDescription", "Object Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, + {"R_msiObjectIdPropertyDescription", + "Object Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."}, @@ -537,5 +538,8 @@ protected Object[][] getContents() { {"R_unknownUTF8SupportValue", "Unknown value for UTF8 support."}, {"R_illegalWKT", "Illegal Well-Known text. Please make sure Well-Known text is valid."}, {"R_illegalTypeForGeometry", "{0} is not supported for Geometry."}, - {"R_illegalWKTposition", "Illegal character in Well-Known text at position {0}."},}; + {"R_illegalWKTposition", "Illegal character in Well-Known text at position {0}."}, + {"R_ADALMissing", "Failed to load ADAL4J Java library for performing {0} authentication."}, + {"R_DLLandADALMissing", + "Failed to load both sqljdbc_auth.dll and ADAL4J Java library for performing {0} authentication. Please install one of them to proceed."}}; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java index 3a46abe1f..b121d2c9e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java @@ -9,23 +9,28 @@ class SqlFedAuthToken { - final Date expiresOn; - final String accessToken; + Date expiresOn; + String accessToken; + final String refreshToken; - SqlFedAuthToken(final String accessToken, final long expiresIn) { + SqlFedAuthToken(String accessToken, long expiresIn, final String refreshToken) { this.accessToken = accessToken; + this.refreshToken = refreshToken; Date now = new Date(); now.setTime(now.getTime() + (expiresIn * 1000)); this.expiresOn = now; } - SqlFedAuthToken(final String accessToken, final Date expiresOn) { + SqlFedAuthToken(String accessToken, Date expiresOn, String refreshToken) { this.accessToken = accessToken; this.expiresOn = expiresOn; + this.refreshToken = refreshToken; } - Date getExpiresOnDate() { - return expiresOn; + void updateAccessToken(String accessToken, Date expiresOnDate) { + this.accessToken = accessToken; + this.expiresOn = expiresOnDate; } -} \ No newline at end of file + +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index 3d189fe2c..70a9edde9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -11,6 +11,7 @@ import java.net.UnknownHostException; import java.text.DecimalFormat; import java.text.MessageFormat; +import java.util.Date; import java.util.Locale; import java.util.Properties; import java.util.Set; @@ -931,6 +932,34 @@ else if (("" + value).contains("E")) { return 0; } + // If the access token is expiring within next 10 minutes, lets just re-create a token for this connection attempt. + // If the token is expiring within the next 45 mins, try to fetch a new token if there is no thread already doing + // it. + // If a thread is already doing the refresh, just use the existing token and proceed. + static synchronized boolean checkIfNeedNewAccessToken(SQLServerConnection connection) { + Date accessTokenExpireDate = connection.getAuthenticationResult().expiresOn; + Date now = new Date(); + + // if the token's expiration is within the next 45 mins + // 45 mins * 60 sec/min * 1000 millisec/sec + if ((accessTokenExpireDate.getTime() - now.getTime()) < (45 * 60 * 1000)) { + + // within the next 10 mins + if ((accessTokenExpireDate.getTime() - now.getTime()) < (10 * 60 * 1000)) { + return true; + } else { + // check if another thread is already updating the access token + if (connection.attemptRefreshTokenLocked) { + return false; + } else { + connection.attemptRefreshTokenLocked = true; + return true; + } + } + } + return false; + } + static final boolean use43Wrapper; static { From 9ba36430d0cd55b9a100cdd64a7ce4b6ffbc920a Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 20 Nov 2018 12:29:08 -0800 Subject: [PATCH 09/22] Add support for Azure App Service and Functions --- .../sqlserver/jdbc/SQLServerConnection.java | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 2e2b1267b..13c00fa88 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -31,7 +31,9 @@ import java.sql.SQLXML; import java.sql.Savepoint; import java.sql.Statement; +import java.text.DateFormat; import java.text.MessageFormat; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -136,11 +138,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private String originalHostNameInCertificate = null; - protected SqlFedAuthToken getAuthenticationResult() { return fedAuthToken; } - + private Boolean isAzureDW = null; static class CityHash128Key implements java.io.Serializable { @@ -394,7 +395,9 @@ class ActiveDirectoryAuthentication { static final String AZURE_REST_MSI_URL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01"; static final String ADAL_GET_ACCESS_TOKEN_FUNCTION_NAME = "ADALGetAccessToken"; static final String ACCESS_TOKEN_IDENTIFIER = "\"access_token\":\""; - static final String ACCESS_TOKEN_EXPIRY_IDENTIFIER = "\"expires_in\":\""; + static final String ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER = "\"expires_in\":\""; + static final String ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER = "\"expires_on\":\""; + static final String ACCESS_TOKEN_EXPIRES_ON_DATE_FORMAT = "M/d/yyyy h:mm:ss a X"; static final int GET_ACCESS_TOKEN_SUCCESS = 0; static final int GET_ACCESS_TOKEN_INVALID_GRANT = 1; static final int GET_ACCESS_TOKEN_TANSISENT_ERROR = 2; @@ -4082,8 +4085,6 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) } private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { - SqlFedAuthInfo sqlFedAuthInfo = new SqlFedAuthInfo(); - // fedAuthInfo should not be null. assert null != fedAuthInfo; @@ -4096,7 +4097,8 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe while (true) { if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { validateAdalLibrary("R_ADALMissing"); - fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); + fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, + authenticationString); // Break out of the retry loop in successful case. break; @@ -4182,8 +4184,8 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe // so we don't need to check the // OS version here. else { - //Check if ADAL4J library is available - validateAdalLibrary("R_DLLandADALMissing"); + // Check if ADAL4J library is available + validateAdalLibrary("R_DLLandADALMissing"); fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthTokenIntegrated(fedAuthInfo, authenticationString); } // Break out of the retry loop in successful case. @@ -4205,7 +4207,17 @@ private void validateAdalLibrary(String errorMessage) throws SQLServerException } private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws SQLServerException { - String urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; + String urlString; + String msiEndpoint = System.getenv("MSI_ENDPOINT"); + String msiSecret = System.getenv("MSI_SECRET"); + boolean isAzureFunction = null != msiEndpoint && !msiEndpoint.isEmpty() && null != msiSecret + && !msiSecret.isEmpty(); + + if (isAzureFunction) { + urlString = msiEndpoint + "?api-version=2017-09-01&resource=" + resource; + } else { + urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; + } if (null != objectId && !objectId.isEmpty()) { urlString += "&object_id=" + objectId; @@ -4216,7 +4228,18 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws try { connection = (HttpURLConnection) new URL(urlString).openConnection(); connection.setRequestMethod("GET"); - connection.setRequestProperty("Metadata", "true"); + + if (isAzureFunction) { + connection.setRequestProperty("Secret", msiSecret); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " Using Azure Function/App Service MSI auth: " + urlString); + } + } else { + connection.setRequestProperty("Metadata", "true"); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " Using Azure MSI auth: " + urlString); + } + } connection.connect(); @@ -4227,14 +4250,32 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws int startIndex_AT = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) + ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER.length(); - int startIndex_ATX = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRY_IDENTIFIER) - + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRY_IDENTIFIER.length(); String accessToken = result.substring(startIndex_AT, result.indexOf("\"", startIndex_AT + 1)); - String accessTokenExpiry = result.substring(startIndex_ATX, result.indexOf("\"", startIndex_ATX + 1)); Calendar cal = new Calendar.Builder().setInstant(new Date()).build(); - cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); + + if (isAzureFunction) { + int startIndex_ATX = result + .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER.length(); + String accessTokenExpiry = result.substring(startIndex_ATX, + result.indexOf("\"", startIndex_ATX + 1)); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " MSI auth token expires on: " + accessTokenExpiry); + } + + DateFormat df = new SimpleDateFormat( + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_DATE_FORMAT); + cal = new Calendar.Builder().setInstant(df.parse(accessTokenExpiry)).build(); + } else { + int startIndex_ATX = result + .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER.length(); + String accessTokenExpiry = result.substring(startIndex_ATX, + result.indexOf("\"", startIndex_ATX + 1)); + cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); + } return new SqlFedAuthToken(accessToken, cal.getTime(), null); } From 5841982982a9a8437ff4b5bebc68ee5ea92594ce Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 22 Nov 2018 11:20:30 -0800 Subject: [PATCH 10/22] Minor Fix in PooledConnection --- .../sqlserver/jdbc/SQLServerPooledConnection.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index fe63e4c0b..bb41255fb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -99,11 +99,15 @@ public Connection getConnection() throws SQLException { if (null != lastProxyConnection) { // if there was a last proxy connection send reset physicalConnection.resetPooledConnection(); - if (pcLogger.isLoggable(Level.FINE) && !lastProxyConnection.isClosed()) - pcLogger.fine(toString() + "proxy " + lastProxyConnection.toString() - + " is not closed before getting the connection."); - // use internal close so there wont be an event due to us closing the connection, if not closed already. - lastProxyConnection.internalClose(); + if (!lastProxyConnection.isClosed()) { + if (pcLogger.isLoggable(Level.FINE)) { + pcLogger.fine(toString() + "proxy " + lastProxyConnection.toString() + + " is not closed before getting the connection."); + } + // use internal close so there wont be an event due to us closing the connection, if not closed + // already. + lastProxyConnection.internalClose(); + } } lastProxyConnection = new SQLServerConnectionPoolProxy(physicalConnection); From 5c04311cf183361acbd81df3aba690ef7f1dd73e Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 23 Nov 2018 09:41:46 -0800 Subject: [PATCH 11/22] Fix PooledConnection refresh token logic --- .../sqlserver/jdbc/SQLServerADAL4JUtils.java | 72 ++++++------------- .../sqlserver/jdbc/SQLServerConnection.java | 29 ++------ .../jdbc/SQLServerPooledConnection.java | 48 +++++++++---- .../sqlserver/jdbc/SqlFedAuthToken.java | 7 +- 4 files changed, 66 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java index ddcad8c3d..a94ca3cc5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerADAL4JUtils.java @@ -38,23 +38,25 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String use AuthenticationResult authenticationResult = future.get(); - return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate(), - authenticationResult.getRefreshToken()); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); } catch (MalformedURLException | InterruptedException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_ADALExecution")); Object[] msgArgs = {user, authenticationString}; - // the cause error message uses \\n\\r which does not give correct format - // change it to \r\n to provide correct format + /* + * the cause error message uses \\n\\r which does not give correct format change it to \r\n to provide + * correct format + */ String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n"); AuthenticationException correctedAuthenticationException = new AuthenticationException( correctedErrorMessage); - // SQLServerException is caused by ExecutionException, which is caused by - // AuthenticationException - // to match the exception tree before error message correction + /* + * SQLServerException is caused by ExecutionException, which is caused by AuthenticationException to match + * the exception tree before error message correction + */ ExecutionException correctedExecutionException = new ExecutionException(correctedAuthenticationException); throw new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException); @@ -68,8 +70,10 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, ExecutorService executorService = Executors.newFixedThreadPool(1); try { - // principal name does not matter, what matters is the realm name - // it gets the username in principal_name@realm_name format + /* + * principal name does not matter, what matters is the realm name it gets the username in + * principal_name@realm_name format + */ KerberosPrincipal kerberosPrincipal = new KerberosPrincipal("username"); String username = kerberosPrincipal.getName(); @@ -83,8 +87,7 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, AuthenticationResult authenticationResult = future.get(); - return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate(), - authenticationResult.getRefreshToken()); + return new SqlFedAuthToken(authenticationResult.getAccessToken(), authenticationResult.getExpiresOnDate()); } catch (InterruptedException | IOException e) { throw new SQLServerException(e.getMessage(), e); } catch (ExecutionException e) { @@ -95,15 +98,18 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, // the case when Future's outcome has no AuthenticationResult but exception throw new SQLServerException(form.format(msgArgs), null); } else { - // the cause error message uses \\n\\r which does not give correct format - // change it to \r\n to provide correct format + /* + * the cause error message uses \\n\\r which does not give correct format change it to \r\n to provide + * correct format + */ String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n"); AuthenticationException correctedAuthenticationException = new AuthenticationException( correctedErrorMessage); - // SQLServerException is caused by ExecutionException, which is caused by - // AuthenticationException - // to match the exception tree before error message correction + /* + * SQLServerException is caused by ExecutionException, which is caused by AuthenticationException to + * match the exception tree before error message correction + */ ExecutionException correctedExecutionException = new ExecutionException( correctedAuthenticationException); @@ -113,38 +119,4 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, executorService.shutdown(); } } - - static SqlFedAuthToken aquireTokenFromRefreshToken(SqlFedAuthInfo fedAuthInfo, SqlFedAuthToken fedAuthToken, - String authenticationString) { - ExecutorService executorService = Executors.newFixedThreadPool(1); - try { - // principal name does not matter, what matters is the realm name - // it gets the username in principal_name@realm_name format - KerberosPrincipal kerberosPrincipal = new KerberosPrincipal("username"); - - if (adal4jLogger.isLoggable(Level.FINE)) { - adal4jLogger.fine(adal4jLogger.toString() + " realm name is:" + kerberosPrincipal.getRealm()); - } - - AuthenticationContext context = new AuthenticationContext(fedAuthInfo.stsurl, false, executorService); - Future future = context.acquireTokenByRefreshToken(fedAuthToken.refreshToken, - ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID, null); - - AuthenticationResult authenticationResult = future.get(); - fedAuthToken.updateAccessToken(authenticationResult.getAccessToken(), - authenticationResult.getExpiresOnDate()); - - return fedAuthToken; - } catch (InterruptedException | IOException | ExecutionException e) { - // We do not want to throw any exception here as we will try again to acquire a - // fresh access Token. In case of any exception, simply log message as INFO - if (adal4jLogger.isLoggable(Level.INFO)) { - adal4jLogger.info(adal4jLogger.toString() + " Failed to acquire access token using refresh Token"); - } - return null; - } finally { - executorService.shutdown(); - } - } - } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 1ff7d39f6..121755607 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1070,34 +1070,17 @@ void checkClosed() throws SQLServerException { SQLServerException.makeFromDriverError(null, null, SQLServerException.getErrString("R_connectionIsClosed"), null, false); } + } + protected boolean needsReconnect() throws SQLServerException { // Check if federated Authentication is in use if (null != fedAuthToken) { // Check if access token is about to expire soon if (Util.checkIfNeedNewAccessToken(this)) { - boolean needsReconnect = false; - // Check if no refreshToken was received - // Applies to cases where nativeDLL is in use - if (null != fedAuthToken.refreshToken) { - // Attempt to refresh access token using refresh token - fedAuthToken = SQLServerADAL4JUtils.aquireTokenFromRefreshToken(sqlFedAuthInfo, fedAuthToken, - authenticationString); - attemptRefreshTokenLocked = false; - // Check if refreshing Access token has failed, would need reconnect - if (null == fedAuthToken) { - needsReconnect = true; - } - } else { - // Reconnect is needed if Native DLL is in use by user application. - needsReconnect = true; - } - if (needsReconnect) { - // Try acquiring a fresh access token now. - connect(this.activeConnectionProperties, null); - } + return true; } } - + return false; } /** @@ -4125,7 +4108,7 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe byte[] accessTokenFromDLL = dllInfo.accessTokenBytes; String accessToken = new String(accessTokenFromDLL, UTF_16LE); - fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn, null); + fedAuthToken = new SqlFedAuthToken(accessToken, dllInfo.expiresIn); // Break out of the retry loop in successful case. break; @@ -4277,7 +4260,7 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); } - return new SqlFedAuthToken(accessToken, cal.getTime(), null); + return new SqlFedAuthToken(accessToken, cal.getTime()); } } catch (Exception e) { SQLServerException.makeFromDriverError(this, null, e.getMessage(), null, true); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index bb41255fb..9b9554c55 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -29,11 +29,11 @@ public class SQLServerPooledConnection implements PooledConnection { private SQLServerConnectionPoolProxy lastProxyConnection; private String factoryUser, factoryPassword; private java.util.logging.Logger pcLogger; - static private final AtomicInteger basePooledConnectionID = new AtomicInteger(0); // Unique id generator for each - // PooledConnection instance - // (used for logging). private final String traceID; + // Unique id generator for each PooledConnection instance (used for logging). + static private final AtomicInteger basePooledConnectionID = new AtomicInteger(0); + SQLServerPooledConnection(SQLServerDataSource ds, String user, String password) throws SQLException { listeners = new Vector<>(); // Piggyback SQLServerDataSource logger for now. @@ -65,7 +65,12 @@ public String toString() { return traceID; } - // Helper function to create a new connection for the pool. + /** + * Helper function to create a new connection for the pool. + * + * @return SQLServerConnection instance + * @throws SQLException + */ private SQLServerConnection createNewConnection() throws SQLException { return factoryDataSource.getConnectionInternal(factoryUser, factoryPassword, this); } @@ -88,24 +93,41 @@ public Connection getConnection() throws SQLException { SQLServerException.getErrString("R_physicalConnectionIsClosed"), "", true); } - // Check with security manager to insure caller has rights to connect. - // This will throw a SecurityException if the caller does not have proper rights. + /* + * Check with security manager to insure caller has rights to connect. This will throw a SecurityException + * if the caller does not have proper rights. + */ physicalConnection.doSecurityCheck(); if (pcLogger.isLoggable(Level.FINE)) pcLogger.fine(toString() + " Physical connection, " + safeCID()); - // The last proxy connection handle returned will be invalidated (moved to closed state) - // when getConnection is called. + /* + * The last proxy connection handle returned will be invalidated (moved to closed state) when getConnection + * is called. + */ if (null != lastProxyConnection) { // if there was a last proxy connection send reset physicalConnection.resetPooledConnection(); + + // Check if a new access token needs to be generated for federated authentication + if (physicalConnection.needsReconnect()) { + /* + * Closing physicalConnection before reconnecting is safe as only one active connection is + * maintained here. + */ + physicalConnection.close(); + physicalConnection.connect(physicalConnection.activeConnectionProperties, null); + } + if (!lastProxyConnection.isClosed()) { if (pcLogger.isLoggable(Level.FINE)) { pcLogger.fine(toString() + "proxy " + lastProxyConnection.toString() + " is not closed before getting the connection."); } - // use internal close so there wont be an event due to us closing the connection, if not closed - // already. + /* + * use internal close so there wont be an event due to us closing the connection, if not closed + * already. + */ lastProxyConnection.internalClose(); } } @@ -220,8 +242,10 @@ private static int nextPooledConnectionID() { return basePooledConnectionID.incrementAndGet(); } - // Helper function to return connectionID of the physicalConnection in a safe manner for logging. - // Returns (null) if physicalConnection is null, otherwise returns connectionID. + /** + * Helper function to return connectionID of the physicalConnection in a safe manner for logging. Returns (null) if + * physicalConnection is null, otherwise returns connectionID. + **/ private String safeCID() { if (null == physicalConnection) return " ConnectionID:(null)"; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java index b121d2c9e..3a791d3ea 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java @@ -11,21 +11,18 @@ class SqlFedAuthToken { Date expiresOn; String accessToken; - final String refreshToken; - SqlFedAuthToken(String accessToken, long expiresIn, final String refreshToken) { + SqlFedAuthToken(String accessToken, long expiresIn) { this.accessToken = accessToken; - this.refreshToken = refreshToken; Date now = new Date(); now.setTime(now.getTime() + (expiresIn * 1000)); this.expiresOn = now; } - SqlFedAuthToken(String accessToken, Date expiresOn, String refreshToken) { + SqlFedAuthToken(String accessToken, Date expiresOn) { this.accessToken = accessToken; this.expiresOn = expiresOn; - this.refreshToken = refreshToken; } void updateAccessToken(String accessToken, Date expiresOnDate) { From eb8b54acb13a6958c4bf0a42794016a56da16703 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Sun, 25 Nov 2018 20:00:43 -0800 Subject: [PATCH 12/22] Revert to original design --- .../sqlserver/jdbc/SQLServerConnection.java | 36 ++++++++++--------- .../sqlserver/jdbc/SqlFedAuthToken.java | 10 ++---- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 121755607..3bf40fe21 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -134,11 +134,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private byte[] accessTokenInByte = null; private SqlFedAuthToken fedAuthToken = null; - private SqlFedAuthInfo sqlFedAuthInfo = null; private String originalHostNameInCertificate = null; - protected SqlFedAuthToken getAuthenticationResult() { + SqlFedAuthToken getAuthenticationResult() { return fedAuthToken; } @@ -3886,7 +3885,7 @@ final void processEnvChange(TDSReader tdsReader) throws SQLServerException { } final void processFedAuthInfo(TDSReader tdsReader, TDSTokenHandler tdsTokenHandler) throws SQLServerException { - sqlFedAuthInfo = new SqlFedAuthInfo(); + SqlFedAuthInfo sqlFedAuthInfo = new SqlFedAuthInfo(); tdsReader.readUnsignedByte(); // token type, 0xEE @@ -4031,16 +4030,16 @@ final void processFedAuthInfo(TDSReader tdsReader, TDSTokenHandler tdsTokenHandl final class FedAuthTokenCommand extends UninterruptableTDSCommand { TDSTokenHandler tdsTokenHandler = null; - String accessToken = null; + SqlFedAuthToken sqlFedAuthToken = null; - FedAuthTokenCommand(String accessToken, TDSTokenHandler tdsTokenHandler) { + FedAuthTokenCommand(SqlFedAuthToken sqlFedAuthToken, TDSTokenHandler tdsTokenHandler) { super("FedAuth"); this.tdsTokenHandler = tdsTokenHandler; - this.accessToken = accessToken; + this.sqlFedAuthToken = sqlFedAuthToken; } final boolean doExecute() throws SQLServerException { - sendFedAuthToken(this, accessToken, tdsTokenHandler); + sendFedAuthToken(this, sqlFedAuthToken, tdsTokenHandler); return true; } } @@ -4055,19 +4054,23 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) || (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) || authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()) && fedAuthRequiredPreLoginResponse); + assert null != fedAuthInfo; - SqlFedAuthToken fedAuthToken = getFedAuthToken(fedAuthInfo); + attemptRefreshTokenLocked = true; + fedAuthToken = getFedAuthToken(fedAuthInfo); + attemptRefreshTokenLocked = false; // fedAuthToken cannot be null. assert null != fedAuthToken; - assert null != fedAuthToken.accessToken; - TDSCommand fedAuthCommand = new FedAuthTokenCommand(fedAuthToken.accessToken, tdsTokenHandler); + TDSCommand fedAuthCommand = new FedAuthTokenCommand(fedAuthToken, tdsTokenHandler); fedAuthCommand.execute(tdsChannel.getWriter(), tdsChannel.getReader(fedAuthCommand)); } private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLServerException { + SqlFedAuthToken fedAuthToken = null; + // fedAuthInfo should not be null. assert null != fedAuthInfo; @@ -4291,9 +4294,10 @@ protected boolean isAccessTokenExpired() { /** * Send the access token to the server. */ - private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, String accessToken, + private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, SqlFedAuthToken fedAuthToken, TDSTokenHandler tdsTokenHandler) throws SQLServerException { - assert null != accessToken; + assert null != fedAuthToken; + assert null != fedAuthToken.accessToken; if (connectionlogger.isLoggable(Level.FINER)) { connectionlogger.fine(toString() + " Sending federated authentication token."); @@ -4301,17 +4305,17 @@ private void sendFedAuthToken(FedAuthTokenCommand fedAuthCommand, String accessT TDSWriter tdsWriter = fedAuthCommand.startRequest(TDS.PKT_FEDAUTH_TOKEN_MESSAGE); - byte[] accessTokenBytes = accessToken.getBytes(UTF_16LE); + byte[] accessToken = fedAuthToken.accessToken.getBytes(UTF_16LE); // Send total length (length of token plus 4 bytes for the token length field) // If we were sending a nonce, this would include that length as well - tdsWriter.writeInt(accessTokenBytes.length + 4); + tdsWriter.writeInt(accessToken.length + 4); // Send length of token - tdsWriter.writeInt(accessTokenBytes.length); + tdsWriter.writeInt(accessToken.length); // Send federated authentication access token. - tdsWriter.writeBytes(accessTokenBytes, 0, accessTokenBytes.length); + tdsWriter.writeBytes(accessToken, 0, accessToken.length); TDSReader tdsReader; tdsReader = fedAuthCommand.startResponse(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java index 3a791d3ea..21aedbb8e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SqlFedAuthToken.java @@ -9,8 +9,8 @@ class SqlFedAuthToken { - Date expiresOn; - String accessToken; + final Date expiresOn; + final String accessToken; SqlFedAuthToken(String accessToken, long expiresIn) { this.accessToken = accessToken; @@ -24,10 +24,4 @@ class SqlFedAuthToken { this.accessToken = accessToken; this.expiresOn = expiresOn; } - - void updateAccessToken(String accessToken, Date expiresOnDate) { - this.accessToken = accessToken; - this.expiresOn = expiresOnDate; - } - } From 459ced843180f1308fbaae52902433a03560673e Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 11 Dec 2018 22:53:36 -0800 Subject: [PATCH 13/22] Fix PooledConnection --- .../jdbc/SQLServerPooledConnection.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index 9b9554c55..2e03f2824 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -101,6 +101,14 @@ public Connection getConnection() throws SQLException { if (pcLogger.isLoggable(Level.FINE)) pcLogger.fine(toString() + " Physical connection, " + safeCID()); + if (null != physicalConnection.getAuthenticationResult()) { + // Check if a new access token needs to be generated for federated authentication + if (Util.checkIfNeedNewAccessToken(physicalConnection)) { + physicalConnection.close(); + physicalConnection = createNewConnection(); + } + } + /* * The last proxy connection handle returned will be invalidated (moved to closed state) when getConnection * is called. @@ -109,16 +117,6 @@ public Connection getConnection() throws SQLException { // if there was a last proxy connection send reset physicalConnection.resetPooledConnection(); - // Check if a new access token needs to be generated for federated authentication - if (physicalConnection.needsReconnect()) { - /* - * Closing physicalConnection before reconnecting is safe as only one active connection is - * maintained here. - */ - physicalConnection.close(); - physicalConnection.connect(physicalConnection.activeConnectionProperties, null); - } - if (!lastProxyConnection.isClosed()) { if (pcLogger.isLoggable(Level.FINE)) { pcLogger.fine(toString() + "proxy " + lastProxyConnection.toString() From 7297bde88170023dd361a97a6c1c46c82e4b8efd Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 17 Dec 2018 14:58:59 -0800 Subject: [PATCH 14/22] Change MSI Object ID to Client ID --- .../sqlserver/jdbc/SQLServerConnection.java | 16 ++++++++++------ .../sqlserver/jdbc/SQLServerDataSource.java | 7 +++---- .../sqlserver/jdbc/SQLServerDriver.java | 6 +++--- .../sqlserver/jdbc/SQLServerResource.java | 4 ++-- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 3bf40fe21..d406b909e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1876,7 +1876,7 @@ else if (0 == requestedPacketSize) activeConnectionProperties.setProperty(sPropKey, SSLProtocol.valueOfString(sPropValue).toString()); } - sPropKey = SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(); + sPropKey = SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); if (null != sPropValue) { activeConnectionProperties.setProperty(sPropKey, sPropValue); @@ -4089,8 +4089,8 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe // Break out of the retry loop in successful case. break; } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { - fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, - activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString())); + fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, activeConnectionProperties + .getProperty(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString())); // Break out of the retry loop in successful case. break; @@ -4192,7 +4192,7 @@ private void validateAdalLibrary(String errorMessage) throws SQLServerException } } - private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws SQLServerException { + private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) throws SQLServerException { String urlString; String msiEndpoint = System.getenv("MSI_ENDPOINT"); String msiSecret = System.getenv("MSI_SECRET"); @@ -4205,8 +4205,12 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String objectId) throws urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; } - if (null != objectId && !objectId.isEmpty()) { - urlString += "&object_id=" + objectId; + if (null != msiClientId && !msiClientId.isEmpty()) { + if (isAzureFunction) { + urlString += "&clientid=" + msiClientId; + } else { + urlString += "&client_id=" + msiClientId; + } } HttpURLConnection connection = null; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index abf3cb677..f704cba04 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -868,14 +868,13 @@ public String getJASSConfigurationName() { @Override public void setMSIObjectId(String msiObjectId) { - setStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), - msiObjectId); + setStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), msiObjectId); } @Override public String getMSIObjectId() { - return getStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), - SQLServerDriverStringProperty.MSI_OBJECT_ID.getDefaultValue()); + return getStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), + SQLServerDriverStringProperty.MSI_CLIENT_ID.getDefaultValue()); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 7d3ba8310..222a897dc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -284,7 +284,7 @@ enum SQLServerDriverStringProperty { KEY_STORE_SECRET("keyStoreSecret", ""), KEY_STORE_LOCATION("keyStoreLocation", ""), SSL_PROTOCOL("sslProtocol", SSLProtocol.TLS.toString()), - MSI_OBJECT_ID("msiObjectId", ""),; + MSI_CLIENT_ID("msiClientId", ""),; private final String name; private final String defaultValue; @@ -504,8 +504,8 @@ public final class SQLServerDriver implements java.sql.Driver { SQLServerDriverStringProperty.SSL_PROTOCOL.getDefaultValue(), false, new String[] {SSLProtocol.TLS.toString(), SSLProtocol.TLS_V10.toString(), SSLProtocol.TLS_V11.toString(), SSLProtocol.TLS_V12.toString()}), - new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MSI_OBJECT_ID.toString(), - SQLServerDriverStringProperty.MSI_OBJECT_ID.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), + SQLServerDriverStringProperty.MSI_CLIENT_ID.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.toString(), Integer.toString(SQLServerDriverIntProperty.CANCEL_QUERY_TIMEOUT.getDefaultValue()), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_BULK_COPY_FOR_BATCH_INSERT.toString(), diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 1b1e4efa2..f35fa1077 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -245,8 +245,8 @@ protected Object[][] getContents() { {"R_statementPoolingCacheSizePropertyDescription", "This setting specifies the size of the prepared statement cache for a connection. A value less than 1 means no cache."}, {"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."}, - {"R_msiObjectIdPropertyDescription", - "Object Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, + {"R_msiClientIdPropertyDescription", + "Client Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"}, {"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""}, {"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."}, {"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."}, From 4371e8882dd770fec615a397115295fef1d9ab14 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 17 Dec 2018 16:02:44 -0800 Subject: [PATCH 15/22] Use Internal needsReconnect() --- .../microsoft/sqlserver/jdbc/SQLServerConnection.java | 10 +++------- .../sqlserver/jdbc/SQLServerPooledConnection.java | 9 +++------ src/main/java/com/microsoft/sqlserver/jdbc/Util.java | 3 +-- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index d406b909e..c2c07da28 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -137,10 +137,6 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial private String originalHostNameInCertificate = null; - SqlFedAuthToken getAuthenticationResult() { - return fedAuthToken; - } - private Boolean isAzureDW = null; static class CityHash128Key implements java.io.Serializable { @@ -1075,7 +1071,7 @@ protected boolean needsReconnect() throws SQLServerException { // Check if federated Authentication is in use if (null != fedAuthToken) { // Check if access token is about to expire soon - if (Util.checkIfNeedNewAccessToken(this)) { + if (Util.checkIfNeedNewAccessToken(this, fedAuthToken.expiresOn)) { return true; } } @@ -4089,8 +4085,8 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe // Break out of the retry loop in successful case. break; } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { - fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, activeConnectionProperties - .getProperty(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString())); + fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, + activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString())); // Break out of the retry loop in successful case. break; diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java index 2e03f2824..31ca83aa8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPooledConnection.java @@ -101,12 +101,9 @@ public Connection getConnection() throws SQLException { if (pcLogger.isLoggable(Level.FINE)) pcLogger.fine(toString() + " Physical connection, " + safeCID()); - if (null != physicalConnection.getAuthenticationResult()) { - // Check if a new access token needs to be generated for federated authentication - if (Util.checkIfNeedNewAccessToken(physicalConnection)) { - physicalConnection.close(); - physicalConnection = createNewConnection(); - } + if (physicalConnection.needsReconnect()) { + physicalConnection.close(); + physicalConnection = createNewConnection(); } /* diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index 02fb0a944..0f08dbce9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -936,8 +936,7 @@ else if (("" + value).contains("E")) { // If the token is expiring within the next 45 mins, try to fetch a new token if there is no thread already doing // it. // If a thread is already doing the refresh, just use the existing token and proceed. - static synchronized boolean checkIfNeedNewAccessToken(SQLServerConnection connection) { - Date accessTokenExpireDate = connection.getAuthenticationResult().expiresOn; + static synchronized boolean checkIfNeedNewAccessToken(SQLServerConnection connection, Date accessTokenExpireDate) { Date now = new Date(); // if the token's expiration is within the next 45 mins From 053ca0dd1280547eca108a31d4fd666786dd319b Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 17 Dec 2018 16:16:01 -0800 Subject: [PATCH 16/22] Remove unwanted code --- .../sqlserver/jdbc/SQLServerConnection.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c2c07da28..481664b9c 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4275,22 +4275,6 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr } } - /** - * Checks if access token is nearing expiry and a new token is required instead. - * - * @return true if Access Token will expire in next 10 mins or less. false if Access Token has more than 10 mins to - * expire. - */ - protected boolean isAccessTokenExpired() { - Calendar now = new Calendar.Builder().setInstant(new Date()).build(); - // Subtract 10 minutes to allow buffer time for reconnection - now.add(Calendar.MINUTE, -10); - if (fedAuthToken.expiresOn.before(now.getTime())) { - return true; - } - return false; - } - /** * Send the access token to the server. */ From ec379ff7732c33ec0406280cfa0d48d0238a471f Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 18 Dec 2018 11:45:08 -0800 Subject: [PATCH 17/22] Update APIs to MSIClientId --- .../sqlserver/jdbc/ISQLServerDataSource.java | 14 +++++++------- .../sqlserver/jdbc/SQLServerDataSource.java | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 15cc915fe..af603ef7a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -807,17 +807,17 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert); /** - * Sets the object id to be used to retrieve access token from MSI EndPoint. + * Sets the client id to be used to retrieve access token from MSI EndPoint. * - * @param msiObjectId - * Object ID of User Assigned Managed Identity + * @param msiClientId + * Client ID of User Assigned Managed Identity */ - public void setMSIObjectId(String msiObjectId); + public void setMSIClientId(String msiClientId); /** - * Returns the value for the connection property 'msiObjectId'. + * Returns the value for the connection property 'msiClientId'. * - * @return msiObjectId property value + * @return msiClientId property value */ - public String getMSIObjectId(); + public String getMSIClientId(); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index f704cba04..b234e5115 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -867,12 +867,12 @@ public String getJASSConfigurationName() { } @Override - public void setMSIObjectId(String msiObjectId) { - setStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), msiObjectId); + public void setMSIClientId(String msiClientId) { + setStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), msiClientId); } @Override - public String getMSIObjectId() { + public String getMSIClientId() { return getStringProperty(connectionProps, SQLServerDriverStringProperty.MSI_CLIENT_ID.toString(), SQLServerDriverStringProperty.MSI_CLIENT_ID.getDefaultValue()); } From 7bbf6f021f083077828385ffe5d326635de01de8 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 18 Dec 2018 12:07:54 -0800 Subject: [PATCH 18/22] Fix numerous trimming calls --- .../sqlserver/jdbc/SQLServerConnection.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 481664b9c..3c79c86a0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -348,7 +348,7 @@ class FederatedAuthenticationFeatureExtensionData { this.libraryType = libraryType; this.fedAuthRequiredPreLoginResponse = fedAuthRequiredPreLoginResponse; - switch (authenticationString.toUpperCase(Locale.ENGLISH).trim()) { + switch (authenticationString.toUpperCase(Locale.ENGLISH)) { case "ACTIVEDIRECTORYPASSWORD": this.authentication = SqlAuthentication.ActiveDirectoryPassword; break; @@ -1546,7 +1546,7 @@ Connection connectInternal(Properties propsIn, if (sPropValue == null) { sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(); } - authenticationString = SqlAuthentication.valueOfString(sPropValue).toString(); + authenticationString = SqlAuthentication.valueOfString(sPropValue).toString().trim(); if (integratedSecurity && !authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) { @@ -2438,7 +2438,7 @@ private void connectHelper(ServerPortPlaceHolder serverInfo, int timeOutsliceInM */ void Prelogin(String serverName, int portNumber) throws SQLServerException { // Build a TDS Pre-Login packet to send to the server. - if ((!authenticationString.trim().equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) + if ((!authenticationString.equalsIgnoreCase(SqlAuthentication.NotSpecified.toString())) || (null != accessTokenInByte)) { fedAuthRequiredByUser = true; } @@ -3603,11 +3603,9 @@ private void logon(LogonCommand command) throws SQLServerException { // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature // Extension // in Login7, indicating the intent to use Active Directory Authentication Library for SQL Server. - if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString()) - || ((authenticationString.trim() - .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) - || authenticationString.trim() - .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) + if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString()) + || ((authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + || authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) && fedAuthRequiredPreLoginResponse)) { federatedAuthenticationInfoRequested = true; fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData(TDS.TDS_FEDAUTH_LIBRARY_ADAL, @@ -4047,8 +4045,8 @@ final boolean doExecute() throws SQLServerException { void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler) throws SQLServerException { assert (null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString()) && null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString())) - || (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) - || authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()) + || (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString()) + || authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()) && fedAuthRequiredPreLoginResponse); assert null != fedAuthInfo; @@ -4077,21 +4075,20 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe int sleepInterval = 100; while (true) { - if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { + if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())) { validateAdalLibrary("R_ADALMissing"); fedAuthToken = SQLServerADAL4JUtils.getSqlFedAuthToken(fedAuthInfo, user, password, authenticationString); // Break out of the retry loop in successful case. break; - } else if (authenticationString.trim().equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { + } else if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) { fedAuthToken = getMSIAuthToken(fedAuthInfo.spn, activeConnectionProperties.getProperty(SQLServerDriverStringProperty.MSI_CLIENT_ID.toString())); // Break out of the retry loop in successful case. break; - } else if (authenticationString.trim() - .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { + } else if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { // If operating system is windows and sqljdbc_auth is loaded then choose the DLL authentication. if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows") @@ -4184,7 +4181,7 @@ private void validateAdalLibrary(String errorMessage) throws SQLServerException } catch (ClassNotFoundException e) { // throw Exception for missing libraries MessageFormat form = new MessageFormat(SQLServerException.getErrString(errorMessage)); - throw new SQLServerException(form.format(new Object[] {authenticationString.trim()}), null, 0, null); + throw new SQLServerException(form.format(new Object[] {authenticationString}), null, 0, null); } } From 50f17e95c5fd8ef70bd5db98bd72db77a82c2d69 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 18 Dec 2018 15:12:12 -0800 Subject: [PATCH 19/22] Add Retry logic for MSI Rest API call on VM --- .../sqlserver/jdbc/SQLServerConnection.java | 170 ++++++++++++------ .../sqlserver/jdbc/SQLServerResource.java | 6 +- 2 files changed, 120 insertions(+), 56 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 3c79c86a0..c0501b36d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -34,6 +34,7 @@ import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -44,6 +45,7 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; @@ -4186,90 +4188,148 @@ private void validateAdalLibrary(String errorMessage) throws SQLServerException } private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) throws SQLServerException { - String urlString; - String msiEndpoint = System.getenv("MSI_ENDPOINT"); - String msiSecret = System.getenv("MSI_SECRET"); + // IMDS upgrade time can take up to 70s + final int imdsUpgradeTimeInMs = 70 * 1000; + final List retrySlots = new ArrayList<>(); + final String msiEndpoint = System.getenv("MSI_ENDPOINT"); + final String msiSecret = System.getenv("MSI_SECRET"); + + StringBuilder urlString = new StringBuilder(); + int retry = 1, maxRetry = 1; + boolean isAzureFunction = null != msiEndpoint && !msiEndpoint.isEmpty() && null != msiSecret && !msiSecret.isEmpty(); if (isAzureFunction) { - urlString = msiEndpoint + "?api-version=2017-09-01&resource=" + resource; + urlString.append(msiEndpoint).append("?api-version=2017-09-01&resource=").append(resource); } else { - urlString = ActiveDirectoryAuthentication.AZURE_REST_MSI_URL + "&resource=" + resource; + urlString.append(ActiveDirectoryAuthentication.AZURE_REST_MSI_URL).append("&resource=").append(resource); + // Retry acquiring access token upto 20 times due to possible IMDS upgrade (Applies to VM only) + maxRetry = 20; + // Simplified variant of Exponential BackOff + for (int x = 0; x < maxRetry; x++) { + retrySlots.add(500 * ((2 << 1) - 1) / 1000); + } } + // Append Client Id if available if (null != msiClientId && !msiClientId.isEmpty()) { if (isAzureFunction) { - urlString += "&clientid=" + msiClientId; + urlString.append("&clientid=").append(msiClientId); } else { - urlString += "&client_id=" + msiClientId; + urlString.append("&client_id=").append(msiClientId); } } - HttpURLConnection connection = null; + // Loop while maxRetry reaches its limit + while (retry <= maxRetry) { + HttpURLConnection connection = null; - try { - connection = (HttpURLConnection) new URL(urlString).openConnection(); - connection.setRequestMethod("GET"); + try { + connection = (HttpURLConnection) new URL(urlString.toString()).openConnection(); + connection.setRequestMethod("GET"); - if (isAzureFunction) { - connection.setRequestProperty("Secret", msiSecret); - if (connectionlogger.isLoggable(Level.FINER)) { - connectionlogger.finer(toString() + " Using Azure Function/App Service MSI auth: " + urlString); - } - } else { - connection.setRequestProperty("Metadata", "true"); - if (connectionlogger.isLoggable(Level.FINER)) { - connectionlogger.finer(toString() + " Using Azure MSI auth: " + urlString); + if (isAzureFunction) { + connection.setRequestProperty("Secret", msiSecret); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " Using Azure Function/App Service MSI auth: " + urlString); + } + } else { + connection.setRequestProperty("Metadata", "true"); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " Using Azure MSI auth: " + urlString); + } } - } - connection.connect(); + connection.connect(); - try (InputStream stream = connection.getInputStream()) { + try (InputStream stream = connection.getInputStream()) { - BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8), 100); - String result = reader.readLine(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, UTF_8), 100); + String result = reader.readLine(); - int startIndex_AT = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) - + ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER.length(); + int startIndex_AT = result.indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_IDENTIFIER.length(); - String accessToken = result.substring(startIndex_AT, result.indexOf("\"", startIndex_AT + 1)); + String accessToken = result.substring(startIndex_AT, result.indexOf("\"", startIndex_AT + 1)); - Calendar cal = new Calendar.Builder().setInstant(new Date()).build(); + Calendar cal = new Calendar.Builder().setInstant(new Date()).build(); - if (isAzureFunction) { - int startIndex_ATX = result - .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER) - + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER.length(); - String accessTokenExpiry = result.substring(startIndex_ATX, - result.indexOf("\"", startIndex_ATX + 1)); - if (connectionlogger.isLoggable(Level.FINER)) { - connectionlogger.finer(toString() + " MSI auth token expires on: " + accessTokenExpiry); + if (isAzureFunction) { + // Fetch expires_on + int startIndex_ATX = result + .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_IDENTIFIER.length(); + String accessTokenExpiry = result.substring(startIndex_ATX, + result.indexOf("\"", startIndex_ATX + 1)); + if (connectionlogger.isLoggable(Level.FINER)) { + connectionlogger.finer(toString() + " MSI auth token expires on: " + accessTokenExpiry); + } + + DateFormat df = new SimpleDateFormat( + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_DATE_FORMAT); + cal = new Calendar.Builder().setInstant(df.parse(accessTokenExpiry)).build(); + } else { + // Fetch expires_in + int startIndex_ATX = result + .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER) + + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER.length(); + String accessTokenExpiry = result.substring(startIndex_ATX, + result.indexOf("\"", startIndex_ATX + 1)); + cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); } - DateFormat df = new SimpleDateFormat( - ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_ON_DATE_FORMAT); - cal = new Calendar.Builder().setInstant(df.parse(accessTokenExpiry)).build(); + return new SqlFedAuthToken(accessToken, cal.getTime()); + } + } catch (Exception e) { + retry++; + if (retry > maxRetry) { + // Do not retry if maxRetry limit has been reached. + break; } else { - int startIndex_ATX = result - .indexOf(ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER) - + ActiveDirectoryAuthentication.ACCESS_TOKEN_EXPIRES_IN_IDENTIFIER.length(); - String accessTokenExpiry = result.substring(startIndex_ATX, - result.indexOf("\"", startIndex_ATX + 1)); - cal.add(Calendar.SECOND, Integer.parseInt(accessTokenExpiry)); + try { + int responseCode = connection.getResponseCode(); + // Check Error Response Code from Connection + if (responseCode == 410 || responseCode == 429 || responseCode == 404 + || (responseCode >= 500 && responseCode <= 599)) { + try { + int retryTimeoutInMs = retrySlots.get(new Random().nextInt(retry - 1)); + // Error code 410 indicates IMDS upgrade is in progress, which can take up to 70s + retryTimeoutInMs = (responseCode == 410 + && retryTimeoutInMs < imdsUpgradeTimeInMs) ? imdsUpgradeTimeInMs + : retryTimeoutInMs; + Thread.sleep(retryTimeoutInMs); + } catch (InterruptedException ex) { + // Throw runtime exception as driver must not be interrupted here + throw new RuntimeException(ex); + } + } else { + if (null != msiClientId && !msiClientId.isEmpty()) { + SQLServerException.makeFromDriverError(this, null, + SQLServerException.getErrString("R_MSITokenFailureClientId"), null, true); + } else { + SQLServerException.makeFromDriverError(this, null, + SQLServerException.getErrString("R_MSITokenFailure"), null, true); + } + } + } catch (IOException io) { + // Throw error as unexpected if response code not available + SQLServerException.makeFromDriverError(this, null, + SQLServerException.getErrString("R_MSITokenFailureUnexpected"), null, true); + } + } + } finally { + if (connection != null) { + connection.disconnect(); } - - return new SqlFedAuthToken(accessToken, cal.getTime()); - } - } catch (Exception e) { - SQLServerException.makeFromDriverError(this, null, e.getMessage(), null, true); - return null; - } finally { - if (connection != null) { - connection.disconnect(); } } + if (retry > maxRetry) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSITokenAcquireFailure")); + Object[] msgArgs = {maxRetry}; + SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true); + } + return null; } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index f35fa1077..51acdfbb9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -541,5 +541,9 @@ protected Object[][] getContents() { {"R_illegalWKTposition", "Illegal character in Well-Known text at position {0}."}, {"R_ADALMissing", "Failed to load ADAL4J Java library for performing {0} authentication."}, {"R_DLLandADALMissing", - "Failed to load both sqljdbc_auth.dll and ADAL4J Java library for performing {0} authentication. Please install one of them to proceed."}}; + "Failed to load both sqljdbc_auth.dll and ADAL4J Java library for performing {0} authentication. Please install one of them to proceed."}, + {"R_MSITokenFailureClientId", "Couldn't acquire access token from IMDS, verify your clientId."}, + {"R_MSITokenFailure", "Couldn't acquire access token from IMDS"}, + {"R_MSITokenFailureUnexpected", "Couldn't acquire access token, unexpected error occurred."}, + {"R_MSITokenAcquireFailure", "MSI: Failed to acquire token"}}; } From 788783174885304ae061a5ebd55ffe98c9b73824 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 21 Dec 2018 13:23:43 -0800 Subject: [PATCH 20/22] Reflect comments --- .../sqlserver/jdbc/SQLServerConnection.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index c0501b36d..898f63811 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -45,10 +45,10 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executor; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -4197,6 +4197,11 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr StringBuilder urlString = new StringBuilder(); int retry = 1, maxRetry = 1; + /* + * isAzureFunction is used for identifying if the current client application is running in a Virtual Machine + * (without MSI environment variables) or App Service/Function (with MSI environment variables) as the APIs to + * be called for acquiring MSI Token are different for both cases. + */ boolean isAzureFunction = null != msiEndpoint && !msiEndpoint.isEmpty() && null != msiSecret && !msiSecret.isEmpty(); @@ -4290,10 +4295,10 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr try { int responseCode = connection.getResponseCode(); // Check Error Response Code from Connection - if (responseCode == 410 || responseCode == 429 || responseCode == 404 - || (responseCode >= 500 && responseCode <= 599)) { + if (410 == responseCode || 429 == responseCode || 404 == responseCode + || (500 <= responseCode && 599 >= responseCode)) { try { - int retryTimeoutInMs = retrySlots.get(new Random().nextInt(retry - 1)); + int retryTimeoutInMs = retrySlots.get(ThreadLocalRandom.current().nextInt(retry - 1)); // Error code 410 indicates IMDS upgrade is in progress, which can take up to 70s retryTimeoutInMs = (responseCode == 410 && retryTimeoutInMs < imdsUpgradeTimeInMs) ? imdsUpgradeTimeInMs From 47844c686f55ade8ab70aecbb63255e244fed9af Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 21 Dec 2018 16:27:59 -0800 Subject: [PATCH 21/22] Updated Error Messages --- .../microsoft/sqlserver/jdbc/SQLServerConnection.java | 8 ++++---- .../microsoft/sqlserver/jdbc/SQLServerResource.java | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 898f63811..77beaf6df 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4288,6 +4288,7 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr } } catch (Exception e) { retry++; + // Below code applicable only when !isAzureFunctcion (VM) if (retry > maxRetry) { // Do not retry if maxRetry limit has been reached. break; @@ -4314,7 +4315,7 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr SQLServerException.getErrString("R_MSITokenFailureClientId"), null, true); } else { SQLServerException.makeFromDriverError(this, null, - SQLServerException.getErrString("R_MSITokenFailure"), null, true); + SQLServerException.getErrString("R_MSITokenFailureImds"), null, true); } } } catch (IOException io) { @@ -4330,9 +4331,8 @@ private SqlFedAuthToken getMSIAuthToken(String resource, String msiClientId) thr } } if (retry > maxRetry) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSITokenAcquireFailure")); - Object[] msgArgs = {maxRetry}; - SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), "", true); + SQLServerException.makeFromDriverError(this, null, SQLServerException + .getErrString(isAzureFunction ? "R_MSITokenFailureEndpoint" : "R_MSITokenFailureImds"), null, true); } return null; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 51acdfbb9..90f64a2d9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -542,8 +542,10 @@ protected Object[][] getContents() { {"R_ADALMissing", "Failed to load ADAL4J Java library for performing {0} authentication."}, {"R_DLLandADALMissing", "Failed to load both sqljdbc_auth.dll and ADAL4J Java library for performing {0} authentication. Please install one of them to proceed."}, - {"R_MSITokenFailureClientId", "Couldn't acquire access token from IMDS, verify your clientId."}, - {"R_MSITokenFailure", "Couldn't acquire access token from IMDS"}, - {"R_MSITokenFailureUnexpected", "Couldn't acquire access token, unexpected error occurred."}, - {"R_MSITokenAcquireFailure", "MSI: Failed to acquire token"}}; + {"R_MSITokenFailureImds", "MSI Token failure: Couldn't acquire access token from IMDS"}, + {"R_MSITokenFailureImdsClientId", + "MSI Token failure: Couldn't acquire access token from IMDS, verify your clientId."}, + {"R_MSITokenFailureUnexpected", + "MSI Token failure: Couldn't acquire access token from IMDS, unexpected error occurred."}, + {"R_MSITokenFailureEndpoint", "MSI Token failure: Failed to acquire token from MSI Endpoint"}}; } From 0271ea2816da19adc25fec747a5e75faac85a131 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 24 Dec 2018 15:20:17 -0800 Subject: [PATCH 22/22] Error message updates --- .../com/microsoft/sqlserver/jdbc/SQLServerResource.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 90f64a2d9..d3a6e7303 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -542,10 +542,10 @@ protected Object[][] getContents() { {"R_ADALMissing", "Failed to load ADAL4J Java library for performing {0} authentication."}, {"R_DLLandADALMissing", "Failed to load both sqljdbc_auth.dll and ADAL4J Java library for performing {0} authentication. Please install one of them to proceed."}, - {"R_MSITokenFailureImds", "MSI Token failure: Couldn't acquire access token from IMDS"}, + {"R_MSITokenFailureImds", "MSI Token failure: Failed to acquire access token from IMDS"}, {"R_MSITokenFailureImdsClientId", - "MSI Token failure: Couldn't acquire access token from IMDS, verify your clientId."}, + "MSI Token failure: Failed to acquire access token from IMDS, verify your clientId."}, {"R_MSITokenFailureUnexpected", - "MSI Token failure: Couldn't acquire access token from IMDS, unexpected error occurred."}, + "MSI Token failure: Failed to acquire access token from IMDS, unexpected error occurred."}, {"R_MSITokenFailureEndpoint", "MSI Token failure: Failed to acquire token from MSI Endpoint"}}; }