-
Notifications
You must be signed in to change notification settings - Fork 426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added automatic detection of REALM in SPN needed for Cross Domain authentication. #40
Changes from all commits
8fd821b
d6de090
97c768a
d6706b9
0ab0394
e1aed34
74de140
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,15 +8,22 @@ | |
|
||
package com.microsoft.sqlserver.jdbc; | ||
|
||
import java.lang.reflect.Method; | ||
import java.net.IDN; | ||
import java.net.InetAddress; | ||
import java.net.UnknownHostException; | ||
import java.security.AccessControlContext; | ||
import java.security.AccessController; | ||
import java.security.PrivilegedActionException; | ||
import java.security.PrivilegedExceptionAction; | ||
import java.util.HashMap; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.logging.Level; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
|
||
import javax.naming.NamingException; | ||
import javax.security.auth.Subject; | ||
import javax.security.auth.login.AppConfigurationEntry; | ||
import javax.security.auth.login.Configuration; | ||
|
@@ -30,6 +37,8 @@ | |
import org.ietf.jgss.GSSName; | ||
import org.ietf.jgss.Oid; | ||
|
||
import com.microsoft.sqlserver.jdbc.dns.DNSKerberosLocator; | ||
|
||
/** | ||
* KerbAuthentication for int auth. | ||
*/ | ||
|
@@ -247,6 +256,7 @@ private String makeSpn(String server, | |
// Get user provided SPN string; if not provided then build the generic one | ||
String userSuppliedServerSpn = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.SERVER_SPN.toString()); | ||
|
||
String spn; | ||
if (null != userSuppliedServerSpn) { | ||
// serverNameAsACE is true, translate the user supplied serverSPN to ASCII | ||
if (con.serverNameAsACE()) { | ||
|
@@ -260,6 +270,152 @@ private String makeSpn(String server, | |
else { | ||
spn = makeSpn(address, port); | ||
} | ||
this.spn = enrichSpnWithRealm(spn, null == userSuppliedServerSpn); | ||
if (!this.spn.equals(spn) && authLogger.isLoggable(Level.FINER)){ | ||
authLogger.finer(toString() + "SPN enriched: " + spn + " := " + this.spn); | ||
} | ||
} | ||
|
||
private static final Pattern SPN_PATTERN = Pattern.compile("MSSQLSvc/(.*):([^:@]+)(@.+)?", Pattern.CASE_INSENSITIVE); | ||
|
||
private String enrichSpnWithRealm(String spn, | ||
boolean allowHostnameCanonicalization) { | ||
if (spn == null) { | ||
return spn; | ||
} | ||
Matcher m = SPN_PATTERN.matcher(spn); | ||
if (!m.matches()) { | ||
return spn; | ||
} | ||
if (m.group(3) != null) { | ||
// Realm is already present, no need to enrich, the job has already been done | ||
return spn; | ||
} | ||
String dnsName = m.group(1); | ||
String portOrInstance = m.group(2); | ||
RealmValidator realmValidator = getRealmValidator(dnsName); | ||
String realm = findRealmFromHostname(realmValidator, dnsName); | ||
if (realm == null && allowHostnameCanonicalization) { | ||
// We failed, try with canonical host name to find a better match | ||
try { | ||
String canonicalHostName = InetAddress.getByName(dnsName).getCanonicalHostName(); | ||
realm = findRealmFromHostname(realmValidator, canonicalHostName); | ||
// Since we have a match, our hostname is the correct one (for instance of server | ||
// name was an IP), so we override dnsName as well | ||
dnsName = canonicalHostName; | ||
} | ||
catch (UnknownHostException cannotCanonicalize) { | ||
// ignored, but we are in a bad shape | ||
} | ||
} | ||
if (realm == null) { | ||
return spn; | ||
} | ||
else { | ||
StringBuilder sb = new StringBuilder("MSSQLSvc/"); | ||
sb.append(dnsName).append(":").append(portOrInstance).append("@").append(realm.toUpperCase(Locale.ENGLISH)); | ||
return sb.toString(); | ||
} | ||
} | ||
|
||
private static RealmValidator validator; | ||
|
||
/** | ||
* Find a suitable way of validating a REALM for given JVM. | ||
* | ||
* @param hostnameToTest | ||
* an example hostname we are gonna use to test our realm validator. | ||
* @return a not null realm Validator. | ||
*/ | ||
static RealmValidator getRealmValidator(String hostnameToTest) { | ||
if (validator != null) { | ||
return validator; | ||
} | ||
// JVM Specific, here Sun/Oracle JVM | ||
try { | ||
Class<?> clz = Class.forName("sun.security.krb5.Config"); | ||
Method getInstance = clz.getMethod("getInstance", new Class[0]); | ||
final Method getKDCList = clz.getMethod("getKDCList", new Class[] {String.class}); | ||
final Object instance = getInstance.invoke(null); | ||
RealmValidator oracleRealmValidator = new RealmValidator() { | ||
|
||
@Override | ||
public boolean isRealmValid(String realm) { | ||
try { | ||
Object ret = getKDCList.invoke(instance, realm); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changes made to test if RealmValidator is validating with default realm instead of realm we are attempting to access:
|
||
return ret != null; | ||
} | ||
catch (Exception err) { | ||
return false; | ||
} | ||
} | ||
}; | ||
validator = oracleRealmValidator; | ||
// As explained here: https://github.com/Microsoft/mssql-jdbc/pull/40#issuecomment-281509304 | ||
// The default Oracle Resolution mechanism is not bulletproof | ||
// If it resolves a crappy name, drop it. | ||
if (!validator.isRealmValid("this.might.not.exist." + hostnameToTest)) { | ||
// Our realm validator is well working, return it | ||
authLogger.fine("Kerberos Realm Validator: Using Built-in Oracle Realm Validation method."); | ||
return oracleRealmValidator; | ||
} | ||
authLogger.fine("Kerberos Realm Validator: Detected buggy Oracle Realm Validator, using DNSKerberosLocator."); | ||
} | ||
catch (ReflectiveOperationException notTheRightJVMException) { | ||
// Ignored, we simply are not using the right JVM | ||
authLogger.fine("Kerberos Realm Validator: No Oracle Realm Validator Available, using DNSKerberosLocator."); | ||
} | ||
// No implementation found, default one, not any realm is valid | ||
validator = new RealmValidator() { | ||
@Override | ||
public boolean isRealmValid(String realm) { | ||
try { | ||
return DNSKerberosLocator.isRealmValid(realm); | ||
} | ||
catch (NamingException err) { | ||
return false; | ||
} | ||
} | ||
}; | ||
return validator; | ||
} | ||
|
||
/** | ||
* Try to find a REALM in the different parts of a host name. | ||
* | ||
* @param realmValidator | ||
* a function that return true if REALM is valid and exists | ||
* @param hostname | ||
* the name we are looking a REALM for | ||
* @return the realm if found, null otherwise | ||
*/ | ||
private String findRealmFromHostname(RealmValidator realmValidator, | ||
String hostname) { | ||
if (hostname == null) { | ||
return null; | ||
} | ||
int index = 0; | ||
while (index != -1 && index < hostname.length() - 2) { | ||
String realm = hostname.substring(index); | ||
if (authLogger.isLoggable(Level.FINEST)) { | ||
authLogger.finest(toString() + " looking up REALM candidate " + realm); | ||
} | ||
if (realmValidator.isRealmValid(realm)) { | ||
return realm.toUpperCase(); | ||
} | ||
index = hostname.indexOf(".", index + 1); | ||
if (index != -1) { | ||
index = index + 1; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* JVM Specific implementation to decide whether a realm is valid or not | ||
*/ | ||
interface RealmValidator { | ||
boolean isRealmValid(String realm); | ||
} | ||
|
||
byte[] GenerateClientContext(byte[] pin, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* 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.dns; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add the licence header files to the new classes? You can find them in other classes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DONE |
||
|
||
import java.util.Set; | ||
|
||
import javax.naming.NameNotFoundException; | ||
import javax.naming.NamingException; | ||
|
||
public final class DNSKerberosLocator { | ||
|
||
private DNSKerberosLocator() { | ||
} | ||
|
||
/** | ||
* Tells whether a realm is valid. | ||
* | ||
* @param realmName | ||
* the realm to test | ||
* @return true if realm is valid, false otherwise | ||
* @throws NamingException | ||
* if DNS failed, so realm existence cannot be determined | ||
*/ | ||
public static boolean isRealmValid(String realmName) throws NamingException { | ||
if (realmName == null || realmName.length() < 2) { | ||
return false; | ||
} | ||
if (realmName.startsWith(".")) { | ||
realmName = realmName.substring(1); | ||
} | ||
try { | ||
Set<DNSRecordSRV> records = DNSUtilities.findSrvRecords("_kerberos._udp." + realmName); | ||
return !records.isEmpty(); | ||
} | ||
catch (NameNotFoundException wrongDomainException) { | ||
return false; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we log this exception if occurs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Canonicalisation failure is not really an error, it simply means there is no reverse DNS. Most of the time it might not be an error IMHO, but if we are here, it probably means that Kerberos won't work.
It is just an improvement, but not a case of failure IMHO