Skip to content

Commit

Permalink
Merge pull request #40 from pierresouchay/dev
Browse files Browse the repository at this point in the history
Added automatic detection of REALM in SPN needed for Cross Domain authentication.
  • Loading branch information
AfsanehR-zz authored Apr 17, 2017
2 parents 1bc07b2 + 74de140 commit c1f88c5
Show file tree
Hide file tree
Showing 5 changed files with 466 additions and 0 deletions.
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,16 +8,23 @@

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.text.MessageFormat;
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 @@ -31,6 +38,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 @@ -275,6 +284,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 @@ -288,6 +298,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);
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);
}

/**
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;

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

0 comments on commit c1f88c5

Please sign in to comment.