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

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

There is a newer version: 12.8.1.jre11
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 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);
    }

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