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

com.facebook.airlift.http.client.spnego.SpnegoAuthentication Maven / Gradle / Ivy

The newest version!
package com.facebook.airlift.http.client.spnego;

import com.facebook.airlift.http.client.KerberosNameType;
import com.facebook.airlift.log.Logger;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.sun.security.auth.module.Krb5LoginModule;
import io.airlift.units.Duration;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.Attributes;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;

import javax.annotation.concurrent.GuardedBy;
import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import java.io.File;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.URI;
import java.net.UnknownHostException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED;
import static org.ietf.jgss.GSSContext.INDEFINITE_LIFETIME;
import static org.ietf.jgss.GSSCredential.DEFAULT_LIFETIME;
import static org.ietf.jgss.GSSCredential.INITIATE_ONLY;
import static org.ietf.jgss.GSSName.NT_USER_NAME;

public class SpnegoAuthentication
        implements Authentication
{
    private static final String NEGOTIATE = HttpHeader.NEGOTIATE.asString();
    private static final Logger LOG = Logger.get(SpnegoAuthentication.class);
    private static final Duration MIN_CREDENTIAL_LIFE_TIME = new Duration(60, TimeUnit.SECONDS);

    private static final GSSManager GSS_MANAGER = GSSManager.getInstance();

    private static final Oid SPNEGO_OID;
    private static final Oid KERBEROS_OID;

    static {
        try {
            SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
            KERBEROS_OID = new Oid("1.2.840.113554.1.2.2");
        }
        catch (GSSException e) {
            throw new AssertionError(e);
        }
    }

    private static final RetryPolicy AUTHENTICATION_RETRY_POLICY = new RetryPolicy<>()
            .withBackoff(1, 5, ChronoUnit.SECONDS)
            .withMaxRetries(5)
            .handleIf(SpnegoAuthentication::isNetworkIssue)
            .onRetry(event -> LOG.debug(
                    "Authentication failed on attempt %s, will retry. Exception: %s",
                    event.getAttemptCount(),
                    event.getLastFailure().getMessage()));

    private final File keytab;
    private final File credentialCache;
    private final String servicePrincipalPattern;
    private final String principal;
    private final String remoteServiceName;
    private final boolean useCanonicalHostname;
    private final Oid nameType;

    @GuardedBy("this")
    private Session clientSession;

    public SpnegoAuthentication(
            File keytab,
            File kerberosConfig,
            File credentialCache,
            String servicePrincipalPattern,
            String principal,
            String remoteServiceName,
            KerberosNameType nameType,
            boolean useCanonicalHostname)
    {
        requireNonNull(kerberosConfig, "Kerberos config path is null");
        requireNonNull(remoteServiceName, "Kerberos remote service name is null");
        requireNonNull(nameType, "GSS name type is null");

        this.keytab = keytab;
        this.credentialCache = credentialCache;
        this.servicePrincipalPattern = servicePrincipalPattern;
        this.principal = principal;
        this.remoteServiceName = remoteServiceName;
        this.nameType = nameType.getOid();
        this.useCanonicalHostname = useCanonicalHostname;

        System.setProperty("java.security.krb5.conf", kerberosConfig.getAbsolutePath());
    }

    @Override
    public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes attributes)
    {
        URI normalizedUri = UriUtil.normalizedUri(request.getURI());

        return new Result()
        {
            @Override
            public URI getURI()
            {
                return normalizedUri;
            }

            @Override
            public void apply(Request request)
            {
                Failsafe.with(AUTHENTICATION_RETRY_POLICY).run(() -> authenticate(request));
            }

            private void authenticate(Request request)
            {
                GSSContext context = null;
                try {
                    String servicePrincipal = makeServicePrincipal(servicePrincipalPattern, remoteServiceName, normalizedUri.getHost(), useCanonicalHostname);
                    Session session = getSession();
                    context = doAs(session.getLoginContext().getSubject(), () -> {
                        GSSContext result = GSS_MANAGER.createContext(
                                GSS_MANAGER.createName(servicePrincipal, nameType),
                                SPNEGO_OID,
                                session.getClientCredential(),
                                INDEFINITE_LIFETIME);

                        result.requestMutualAuth(true);
                        result.requestConf(true);
                        result.requestInteg(true);
                        result.requestCredDeleg(false);
                        return result;
                    });

                    byte[] token = context.initSecContext(new byte[0], 0, 0);
                    if (token != null) {
                        request.header(headerInfo.getHeader(), format("%s %s", NEGOTIATE, Base64.getEncoder().encodeToString(token)));
                    }
                    else {
                        throw new RuntimeException(format("No token generated from GSS context for %s", request.getURI()));
                    }
                }
                catch (GSSException e) {
                    throw new RuntimeException(format("Failed to establish GSSContext for request %s", request.getURI()), e);
                }
                catch (LoginException e) {
                    throw new RuntimeException(format("Failed to establish LoginContext for request %s", request.getURI()), e);
                }
                finally {
                    try {
                        if (context != null) {
                            context.dispose();
                        }
                    }
                    catch (GSSException e) {
                        // ignore
                    }
                }
            }
        };
    }

    private static boolean isNetworkIssue(Throwable throwable)
    {
        return Throwables.getCausalChain(throwable).stream()
                .anyMatch(SocketException.class::isInstance);
    }

    @Override
    public boolean matches(String type, URI uri, String realm)
    {
        // The class matches all requests for Negotiate scheme. Realm is not used for now
        return NEGOTIATE.equalsIgnoreCase(type);
    }

    private synchronized Session getSession()
            throws LoginException, GSSException
    {
        if (clientSession == null || clientSession.getClientCredential().getRemainingLifetime() < MIN_CREDENTIAL_LIFE_TIME.getValue(TimeUnit.SECONDS)) {
            // TODO: do we need to call logout() on the LoginContext?

            LoginContext loginContext = new LoginContext("", null, null, new Configuration()
            {
                @Override
                public AppConfigurationEntry[] getAppConfigurationEntry(String name)
                {
                    ImmutableMap.Builder optionsBuilder = ImmutableMap.builder();
                    optionsBuilder.put("refreshKrb5Config", "true");
                    optionsBuilder.put("doNotPrompt", "true");
                    optionsBuilder.put("useKeyTab", "true");
                    if (LOG.isDebugEnabled()) {
                        optionsBuilder.put("debug", "true");
                    }

                    if (keytab != null) {
                        optionsBuilder.put("keyTab", keytab.getAbsolutePath());
                    }

                    if (credentialCache != null) {
                        optionsBuilder.put("ticketCache", credentialCache.getAbsolutePath());
                        optionsBuilder.put("useTicketCache", "true");
                        optionsBuilder.put("renewTGT", "true");
                    }

                    if (principal != null) {
                        optionsBuilder.put("principal", principal);
                    }

                    return new AppConfigurationEntry[] {
                            new AppConfigurationEntry(Krb5LoginModule.class.getName(), REQUIRED, optionsBuilder.build())
                    };
                }
            });

            loginContext.login();
            Subject subject = loginContext.getSubject();
            Principal clientPrincipal = subject.getPrincipals().iterator().next();
            GSSCredential clientCredential = doAs(subject, () -> GSS_MANAGER.createCredential(
                    GSS_MANAGER.createName(clientPrincipal.getName(), NT_USER_NAME),
                    DEFAULT_LIFETIME,
                    KERBEROS_OID,
                    INITIATE_ONLY));

            clientSession = new Session(loginContext, clientCredential);
        }

        return clientSession;
    }

    private static String makeServicePrincipal(String servicePrincipalPattern, String serviceName, String hostName, boolean useCanonicalHostname)
    {
        String serviceHostName = hostName;
        if (useCanonicalHostname) {
            serviceHostName = canonicalizeServiceHostname(hostName);
        }
        return servicePrincipalPattern.replaceAll("\\$\\{SERVICE}", serviceName).replaceAll("\\$\\{HOST}", serviceHostName.toLowerCase(Locale.US));
    }

    private static String canonicalizeServiceHostname(String hostName)
    {
        try {
            InetAddress address = InetAddress.getByName(hostName);
            String fullHostName;
            if ("localhost".equalsIgnoreCase(address.getHostName())) {
                fullHostName = InetAddress.getLocalHost().getCanonicalHostName();
            }
            else {
                fullHostName = address.getCanonicalHostName();
            }
            return fullHostName;
        }
        catch (UnknownHostException e) {
            throw new UncheckedIOException(e);
        }
    }

    private interface GssSupplier
    {
        T get()
                throws GSSException;
    }

    private static  T doAs(Subject subject, GssSupplier action)
    {
        return Subject.doAs(subject, (PrivilegedAction) () -> {
            try {
                return action.get();
            }
            catch (GSSException e) {
                throw new RuntimeException(e);
            }
        });
    }

    private static class Session
    {
        private final LoginContext loginContext;
        private final GSSCredential clientCredential;

        Session(LoginContext loginContext, GSSCredential clientCredential)
        {
            requireNonNull(loginContext, "loginContext is null");
            requireNonNull(clientCredential, "gssCredential is null");

            this.loginContext = loginContext;
            this.clientCredential = clientCredential;
        }

        LoginContext getLoginContext()
        {
            return loginContext;
        }

        GSSCredential getClientCredential()
        {
            return clientCredential;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy