All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.sqlserver.jdbc.KerbAuthentication Maven / Gradle / Ivy

Go to download

Microsoft JDBC Driver for SQL Server. The Azure Key Vault feature in Microsoft JDBC Driver for SQL Server depends on Azure SDK for JAVA and Azure Active Directory Library For Java.

There is a newer version: 12.7.0.jre11-preview
Show newest version
/*
 * 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;

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.Locale;
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.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

import com.microsoft.sqlserver.jdbc.dns.DNSKerberosLocator;

/**
 * KerbAuthentication for int auth.
 */
final class KerbAuthentication extends SSPIAuthentication {
    private final static java.util.logging.Logger authLogger = java.util.logging.Logger
            .getLogger("com.microsoft.sqlserver.jdbc.internals.KerbAuthentication");

    private final SQLServerConnection con;
    private final String spn;

    private final GSSManager manager = GSSManager.getInstance();
    private LoginContext lc = null;
    private GSSCredential peerCredentials = null;
    private GSSContext peerContext = null;

    static {
        // Overrides the default JAAS configuration loader.
        // This one will forward to the default one in all cases but the default configuration is empty.
        Configuration.setConfiguration(new JaasConfiguration(Configuration.getConfiguration()));
    }

    private void intAuthInit() throws SQLServerException {
        try {
            // If we need to support NTLM as well, we can use null
            // Kerberos OID
            Oid kerberos = new Oid("1.2.840.113554.1.2.2");
            // http://blogs.sun.com/harcey/entry/of_java_kerberos_and_access
            // We pass null to indicate that the system should interpret the SPN
            // as it is.
            GSSName remotePeerName = manager.createName(spn, null);

            if (null != peerCredentials) {
                peerContext = manager.createContext(remotePeerName, kerberos, peerCredentials, GSSContext.DEFAULT_LIFETIME);
                peerContext.requestCredDeleg(false);
                peerContext.requestMutualAuth(true);
                peerContext.requestInteg(true);
            }
            else {
                String configName = con.activeConnectionProperties.getProperty(SQLServerDriverStringProperty.JAAS_CONFIG_NAME.toString(),
                                                                               SQLServerDriverStringProperty.JAAS_CONFIG_NAME.getDefaultValue());
                Subject currentSubject;
                KerbCallback callback = new KerbCallback(con);
                try {
                    AccessControlContext context = AccessController.getContext();
                    currentSubject = Subject.getSubject(context);
                    if (null == currentSubject) {
                        lc = new LoginContext(configName, callback);
                        lc.login();
                        // per documentation LoginContext will instantiate a new subject.
                        currentSubject = lc.getSubject();
                    }
                }
                catch (LoginException 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)) {
                    authLogger.finer(toString() + " Getting client credentials");
                }
                peerCredentials = getClientCredential(currentSubject, manager, kerberos);
                if (authLogger.isLoggable(Level.FINER)) {
                    authLogger.finer(toString() + " creating security context");
                }
                
                peerContext = manager.createContext(remotePeerName, kerberos, peerCredentials, GSSContext.DEFAULT_LIFETIME);
                // The following flags should be inline with our native implementation.
                peerContext.requestCredDeleg(true);
                peerContext.requestMutualAuth(true);
                peerContext.requestInteg(true);
            }
        }

        catch (GSSException ge) {
            authLogger.finer(toString() + "initAuthInit failed GSSException:-" + ge);
            con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), ge);
        }
        catch (PrivilegedActionException ge) {
            authLogger.finer(toString() + "initAuthInit failed privileged exception:-" + ge);
            con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), ge);
        }

    }

    // We have to do a privileged action to create the credential of the user in the current context
    private static GSSCredential getClientCredential(final Subject subject,
            final GSSManager MANAGER,
            final Oid kerboid) throws PrivilegedActionException {
        final PrivilegedExceptionAction action = new PrivilegedExceptionAction() {
            public GSSCredential run() throws GSSException {
                return MANAGER.createCredential(null // use the default principal
                , GSSCredential.DEFAULT_LIFETIME, kerboid, GSSCredential.INITIATE_ONLY);
            }
        };
        // TO support java 5, 6 we have to do this
        // The signature for Java 5 returns an object 6 returns GSSCredential, immediate casting throws 
        // warning in Java 6.
        Object credential = Subject.doAs(subject, action);
        return (GSSCredential) credential;
    }

    private byte[] intAuthHandShake(byte[] pin,
            boolean[] done) throws SQLServerException {
        try {
            if (authLogger.isLoggable(Level.FINER)) {
                authLogger.finer(toString() + " Sending token to server over secure context");
            }
            byte[] byteToken = peerContext.initSecContext(pin, 0, pin.length);

            if (peerContext.isEstablished()) {
                done[0] = true;
                if (authLogger.isLoggable(Level.FINER))
                    authLogger.finer(toString() + "Authentication done.");
            }
            else if (null == byteToken) {
                // The documentation is not clear on when this can happen but it does say this could happen
                if (authLogger.isLoggable(Level.INFO)) {
                    authLogger.info(toString() + "byteToken is null in initSecContext.");
                }
                con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"));
            }
            return byteToken;
        }
        catch (GSSException ge) {
            authLogger.finer(toString() + "initSecContext Failed :-" + ge);
            con.terminate(SQLServerException.DRIVER_ERROR_NONE, SQLServerException.getErrString("R_integratedAuthenticationFailed"), ge);
        }
        // keep the compiler happy
        return null;
    }

    private String makeSpn(String server,
            int port) throws SQLServerException {
        if (authLogger.isLoggable(Level.FINER)) {
            authLogger.finer(toString() + " Server: " + server + " port: " + port);
        }
        StringBuilder spn = new StringBuilder("MSSQLSvc/");
        // Format is MSSQLSvc/myhost.domain.company.com:1433
        // FQDN must be provided
        if (con.serverNameAsACE()) {
            spn.append(IDN.toASCII(server));
        }
        else {
            spn.append(server);
        }
        spn.append(":");
        spn.append(port);
        String strSPN = spn.toString();
        if (authLogger.isLoggable(Level.FINER)) {
            authLogger.finer(toString() + " SPN: " + strSPN);
        }
        return strSPN;
    }

    // Package visible members below.
    KerbAuthentication(SQLServerConnection con,
            String address,
            int port) throws SQLServerException {
        this.con = con;
        // 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()) {
                int slashPos = userSuppliedServerSpn.indexOf("/");
                spn = userSuppliedServerSpn.substring(0, slashPos + 1) + IDN.toASCII(userSuppliedServerSpn.substring(slashPos + 1));
            }
            else {
                spn = userSuppliedServerSpn;
            }
        }
        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 non-existing 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);
    }

    /**
     * 
     * @param con
     * @param address
     * @param port
     * @param ImpersonatedUserCred
     * @throws SQLServerException
     */
    KerbAuthentication(SQLServerConnection con,
            String address,
            int port,
            GSSCredential ImpersonatedUserCred) throws SQLServerException {
        this(con, address, port);
        peerCredentials = ImpersonatedUserCred;
    }

    byte[] GenerateClientContext(byte[] pin,
            boolean[] done) throws SQLServerException {
        if (null == peerContext) {
            intAuthInit();
        }
        return intAuthHandShake(pin, done);
    }

    int ReleaseClientContext() throws SQLServerException {
        try {
            if (null != peerCredentials)
                peerCredentials.dispose();
            if (null != peerContext)
                peerContext.dispose();
            if (null != lc)
                lc.logout();
        }
        catch (LoginException e) {
            // yes we are eating exceptions here but this should not fail in the normal circumstances and we do not want to eat previous
            // login errors if caused before which is more useful to the user than the cleanup errors.
            authLogger.fine(toString() + " Release of the credentials failed LoginException: " + e);
        }
        catch (GSSException e) {
            // yes we are eating exceptions here but this should not fail in the normal circumstances and we do not want to eat previous
            // login errors if caused before which is more useful to the user than the cleanup errors.
            authLogger.fine(toString() + " Release of the credentials failed GSSException: " + e);
        }
        return 0;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy