From 290b8bc437dec25f7688d9c457e86f21021524f9 Mon Sep 17 00:00:00 2001 From: Pierre Souchay Date: Tue, 28 Feb 2017 10:42:54 +0100 Subject: [PATCH 1/3] Allow authenticating using Kerberos and a Principal/Password. This patch allow to authenticate using kerberos using the previous methods (eg: keytab) or specifying user/password either in properties or in connect method. This allows to use GUIs for instance to connect using Kerberos without having a sql user. --- .../sqlserver/jdbc/KerbAuthentication.java | 5 +- .../sqlserver/jdbc/KerbCallback.java | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java index d84058e58f..d3fb4aa94b 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java @@ -87,7 +87,6 @@ class SQLJDBCDriverConfig extends Configuration { else { Map confDetails = new HashMap(); confDetails.put("useTicketCache", "true"); - confDetails.put("doNotPrompt", "true"); appConf = new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, confDetails); if (authLogger.isLoggable(Level.FINER)) @@ -144,14 +143,16 @@ private void intAuthInit() throws SQLServerException { AccessControlContext context = AccessController.getContext(); currentSubject = Subject.getSubject(context); if (null == currentSubject) { - lc = new LoginContext(CONFIGNAME); + lc = new LoginContext(CONFIGNAME, new KerbCallback(con)); lc.login(); // per documentation LoginContext will instantiate a new subject. currentSubject = lc.getSubject(); } } catch (LoginException le) { + authLogger.fine("Failed to login due to " + le.getClass().getName() + ":" + le.getMessage()); con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); + return; } if (authLogger.isLoggable(Level.FINER)) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java new file mode 100644 index 0000000000..941d717800 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java @@ -0,0 +1,53 @@ +package com.microsoft.sqlserver.jdbc; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Properties; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +public class KerbCallback implements CallbackHandler { + + private final SQLServerConnection con; + + KerbCallback(SQLServerConnection con) { + this.con = con; + } + + private static String getAnyOf(Callback callback, Properties properties, String... names) + throws UnsupportedCallbackException { + for (String name : names) { + String val = properties.getProperty(name); + if (val != null && !val.trim().isEmpty()) { + return val; + } + } + throw new UnsupportedCallbackException(callback, + "Cannot get any of properties: " + Arrays.toString(names) + " from con properties"); + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(getAnyOf(callback, con.activeConnectionProperties, + "user", SQLServerDriverStringProperty.USER.name())); + } else if (callback instanceof PasswordCallback) { + String password = getAnyOf(callback, con.activeConnectionProperties, + "password", SQLServerDriverStringProperty.PASSWORD.name()); + ((PasswordCallback) callbacks[i]) + .setPassword(password.toCharArray()); + + } else { + throw new UnsupportedCallbackException(callback, "Unrecognized Callback type: " + callback.getClass()); + } + } + + } + +} From c4082f81b1bf164d5acc608b1c66eff14b2c0a75 Mon Sep 17 00:00:00 2001 From: Pierre Souchay Date: Tue, 28 Feb 2017 23:47:11 +0100 Subject: [PATCH 2/3] When Kerberos credentials were wrong, authentication was looping for a long time. This commit solves this by stopping directly authentication when login() fails. It means that if keytab is wrong OR provided principal/password are wrong, the driver immediatly return and does not retry in a endless loop. --- .../sqlserver/jdbc/KerbAuthentication.java | 20 ++++++++++++++++--- .../sqlserver/jdbc/KerbCallback.java | 16 +++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java index d3fb4aa94b..6fdfd3cb5a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java @@ -139,19 +139,33 @@ private void intAuthInit() throws SQLServerException { } else { Subject currentSubject = null; + KerbCallback callback = new KerbCallback(con); try { AccessControlContext context = AccessController.getContext(); currentSubject = Subject.getSubject(context); if (null == currentSubject) { - lc = new LoginContext(CONFIGNAME, new KerbCallback(con)); + lc = new LoginContext(CONFIGNAME, callback); lc.login(); // per documentation LoginContext will instantiate a new subject. currentSubject = lc.getSubject(); } } catch (LoginException le) { - authLogger.fine("Failed to login due to " + le.getClass().getName() + ":" + le.getMessage()); - con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); + if (authLogger.isLoggable(Level.FINE)) { + authLogger.fine(toString() + "Failed to login using Kerberos due to " + le.getClass().getName() + ":" + le.getMessage()); + } + try { + // Not very clean since it raises an Exception, but we are sure we are cleaning well everything + con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); + } catch (SQLServerException alwaysTriggered) { + String message = String.format("%s due to %s (%s)", alwaysTriggered.getMessage(), le.getClass().getName(), le.getMessage()); + if (callback.getUsernameRequested() != null) { + message = String.format("Login failed for Kerberos principal '%s'. %s", callback.getUsernameRequested(), message); + } + // By throwing Exception with LOGON_FAILED -> we avoid looping for connection + // In this case, authentication will never work anyway -> fail fast + throw new SQLServerException(message, alwaysTriggered.getSQLState(), SQLServerException.LOGON_FAILED, le); + } return; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java index 941d717800..bdb38e3e1d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java @@ -13,6 +13,7 @@ public class KerbCallback implements CallbackHandler { private final SQLServerConnection con; + private String usernameRequested = null; KerbCallback(SQLServerConnection con) { this.con = con; @@ -30,13 +31,22 @@ private static String getAnyOf(Callback callback, Properties properties, String. "Cannot get any of properties: " + Arrays.toString(names) + " from con properties"); } + /** + * If a name was retrieved By Kerberos, return it. + * @return null if callback was not called or username was not provided + */ + public String getUsernameRequested(){ + return usernameRequested; + } + @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { Callback callback = callbacks[i]; if (callback instanceof NameCallback) { - ((NameCallback) callback).setName(getAnyOf(callback, con.activeConnectionProperties, - "user", SQLServerDriverStringProperty.USER.name())); + usernameRequested = getAnyOf(callback, con.activeConnectionProperties, + "user", SQLServerDriverStringProperty.USER.name()); + ((NameCallback) callback).setName(usernameRequested); } else if (callback instanceof PasswordCallback) { String password = getAnyOf(callback, con.activeConnectionProperties, "password", SQLServerDriverStringProperty.PASSWORD.name()); @@ -47,7 +57,5 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback throw new UnsupportedCallbackException(callback, "Unrecognized Callback type: " + callback.getClass()); } } - } - } From 115c7e9166b0393178a8276f624509aacbd0909d Mon Sep 17 00:00:00 2001 From: Pierre Souchay Date: Fri, 7 Apr 2017 11:08:21 +0200 Subject: [PATCH 3/3] Reformat code in KerbCallback with formatter Added translated message for Kerberos login authentication errors. --- .../sqlserver/jdbc/KerbAuthentication.java | 7 +++-- .../sqlserver/jdbc/KerbCallback.java | 26 +++++++++---------- .../sqlserver/jdbc/SQLServerResource.java | 2 ++ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java index 6fdfd3cb5a..868b20d7eb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java @@ -13,6 +13,7 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; @@ -158,9 +159,11 @@ private void intAuthInit() throws SQLServerException { // Not very clean since it raises an Exception, but we are sure we are cleaning well everything con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), le); } catch (SQLServerException alwaysTriggered) { - String message = String.format("%s due to %s (%s)", alwaysTriggered.getMessage(), le.getClass().getName(), le.getMessage()); + String message = MessageFormat.format(SQLServerException.getErrString("R_kerberosLoginFailed"), + alwaysTriggered.getMessage(), le.getClass().getName(), le.getMessage()); if (callback.getUsernameRequested() != null) { - message = String.format("Login failed for Kerberos principal '%s'. %s", callback.getUsernameRequested(), message); + message = MessageFormat.format(SQLServerException.getErrString("R_kerberosLoginFailedForUsername"), + callback.getUsernameRequested(), message); } // By throwing Exception with LOGON_FAILED -> we avoid looping for connection // In this case, authentication will never work anyway -> fail fast diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java index bdb38e3e1d..8cdc4cca96 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java @@ -19,23 +19,24 @@ public class KerbCallback implements CallbackHandler { this.con = con; } - private static String getAnyOf(Callback callback, Properties properties, String... names) - throws UnsupportedCallbackException { + private static String getAnyOf(Callback callback, + Properties properties, + String... names) throws UnsupportedCallbackException { for (String name : names) { String val = properties.getProperty(name); if (val != null && !val.trim().isEmpty()) { return val; } } - throw new UnsupportedCallbackException(callback, - "Cannot get any of properties: " + Arrays.toString(names) + " from con properties"); + throw new UnsupportedCallbackException(callback, "Cannot get any of properties: " + Arrays.toString(names) + " from con properties"); } /** * If a name was retrieved By Kerberos, return it. + * * @return null if callback was not called or username was not provided */ - public String getUsernameRequested(){ + public String getUsernameRequested() { return usernameRequested; } @@ -44,16 +45,15 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback for (int i = 0; i < callbacks.length; i++) { Callback callback = callbacks[i]; if (callback instanceof NameCallback) { - usernameRequested = getAnyOf(callback, con.activeConnectionProperties, - "user", SQLServerDriverStringProperty.USER.name()); + usernameRequested = getAnyOf(callback, con.activeConnectionProperties, "user", SQLServerDriverStringProperty.USER.name()); ((NameCallback) callback).setName(usernameRequested); - } else if (callback instanceof PasswordCallback) { - String password = getAnyOf(callback, con.activeConnectionProperties, - "password", SQLServerDriverStringProperty.PASSWORD.name()); - ((PasswordCallback) callbacks[i]) - .setPassword(password.toCharArray()); + } + else if (callback instanceof PasswordCallback) { + String password = getAnyOf(callback, con.activeConnectionProperties, "password", SQLServerDriverStringProperty.PASSWORD.name()); + ((PasswordCallback) callbacks[i]).setPassword(password.toCharArray()); - } else { + } + else { throw new UnsupportedCallbackException(callback, "Unrecognized Callback type: " + callback.getClass()); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 84bc5d3109..5d8c86d9bb 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -380,5 +380,7 @@ protected Object[][] getContents() { {"R_invalidFipsEncryptConfig", "Could not enable FIPS due to either encrypt is not true or using trusted certificate settings."}, {"R_invalidFipsProviderConfig", "Could not enable FIPS due to invalid FIPSProvider or TrustStoreType."}, {"R_serverPreparedStatementDiscardThreshold", "The serverPreparedStatementDiscardThreshold {0} is not valid."}, + {"R_kerberosLoginFailedForUsername", "Cannot login with Kerberos principal {0}, check your credentials. {1}"}, + {"R_kerberosLoginFailed", "Kerberos Login failed: {0} due to {1} ({2})"}, }; }