diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java b/src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java index d84058e58f..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; @@ -87,7 +88,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)) @@ -140,18 +140,36 @@ 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); + lc = new LoginContext(CONFIGNAME, callback); lc.login(); // per documentation LoginContext will instantiate a new subject. currentSubject = lc.getSubject(); } } catch (LoginException le) { - 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 = MessageFormat.format(SQLServerException.getErrString("R_kerberosLoginFailed"), + alwaysTriggered.getMessage(), le.getClass().getName(), le.getMessage()); + if (callback.getUsernameRequested() != null) { + 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 + throw new SQLServerException(message, alwaysTriggered.getSQLState(), SQLServerException.LOGON_FAILED, 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..8cdc4cca96 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/KerbCallback.java @@ -0,0 +1,61 @@ +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; + private String usernameRequested = null; + + 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"); + } + + /** + * 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) { + 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 { + 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})"}, }; }