Skip to content
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

Merged
merged 7 commits into from
Apr 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/KerbAuthentication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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()) {
Expand All @@ -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
Copy link
Contributor

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?

Copy link
Contributor Author

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

}
}
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);

Choose a reason for hiding this comment

The 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:

Object defRet = getKDCList.invoke(instance, realmDefault);

if(ret.toString().equalsIgnoreCase(defRet.toString())) {
    return false;
}

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,
Expand Down
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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}
}
}
Loading