io.druid.security.kerberos.KerberosAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for io.druid.security.kerberos.KerberosAuthenticator.java

Source

/*
 * Licensed to Metamarkets Group Inc. (Metamarkets) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Metamarkets licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package io.druid.security.kerberos;

import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.base.Throwables;
import com.metamx.http.client.HttpClient;
import io.druid.guice.annotations.Self;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.server.DruidNode;
import io.druid.server.security.AuthConfig;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Authenticator;
import org.apache.commons.codec.binary.Base64;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
import org.apache.hadoop.security.authentication.util.KerberosUtil;
import org.apache.hadoop.security.authentication.util.Signer;
import org.apache.hadoop.security.authentication.util.SignerException;
import org.apache.hadoop.security.authentication.util.SignerSecretProvider;
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.util.Attributes;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import sun.security.krb5.EncryptedData;
import sun.security.krb5.EncryptionKey;
import sun.security.krb5.internal.APReq;
import sun.security.krb5.internal.EncTicketPart;
import sun.security.krb5.internal.Krb5;
import sun.security.krb5.internal.Ticket;
import sun.security.krb5.internal.crypto.KeyUsage;
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;

import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosKey;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KeyTab;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@JsonTypeName("kerberos")
public class KerberosAuthenticator implements Authenticator {
    private static final Logger log = new Logger(KerberosAuthenticator.class);
    private static final Pattern HADOOP_AUTH_COOKIE_REGEX = Pattern.compile(".*p=(\\S+)&t=.*");
    public static final List<String> DEFAULT_EXCLUDED_PATHS = Collections.emptyList();

    private final DruidNode node;
    private final String serverPrincipal;
    private final String serverKeytab;
    private final String internalClientPrincipal;
    private final String internalClientKeytab;
    private final String authToLocal;
    private final List<String> excludedPaths;
    private final String cookieSignatureSecret;
    private final String authorizerName;
    private LoginContext loginContext;

    @JsonCreator
    public KerberosAuthenticator(@JsonProperty("serverPrincipal") String serverPrincipal,
            @JsonProperty("serverKeytab") String serverKeytab,
            @JsonProperty("internalClientPrincipal") String internalClientPrincipal,
            @JsonProperty("internalClientKeytab") String internalClientKeytab,
            @JsonProperty("authToLocal") String authToLocal,
            @JsonProperty("excludedPaths") List<String> excludedPaths,
            @JsonProperty("cookieSignatureSecret") String cookieSignatureSecret,
            @JsonProperty("authorizerName") String authorizerName, @JacksonInject @Self DruidNode node) {
        this.node = node;
        this.serverPrincipal = serverPrincipal;
        this.serverKeytab = serverKeytab;
        this.internalClientPrincipal = internalClientPrincipal;
        this.internalClientKeytab = internalClientKeytab;
        this.authToLocal = authToLocal == null ? "DEFAULT" : authToLocal;
        this.excludedPaths = excludedPaths == null ? DEFAULT_EXCLUDED_PATHS : excludedPaths;
        this.cookieSignatureSecret = cookieSignatureSecret;
        this.authorizerName = authorizerName;
    }

    @Override
    public Filter getFilter() {
        return new AuthenticationFilter() {
            private Signer mySigner;

            @Override
            public void init(FilterConfig filterConfig) throws ServletException {
                ClassLoader prevLoader = Thread.currentThread().getContextClassLoader();
                try {
                    // AuthenticationHandler is created during Authenticationfilter.init using reflection with thread context class loader.
                    // In case of druid since the class is actually loaded as an extension and filter init is done in main thread.
                    // We need to set the classloader explicitly to extension class loader.
                    Thread.currentThread().setContextClassLoader(AuthenticationFilter.class.getClassLoader());
                    super.init(filterConfig);
                    String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
                    configPrefix = (configPrefix != null) ? configPrefix + "." : "";
                    Properties config = getConfiguration(configPrefix, filterConfig);
                    String signatureSecret = config.getProperty(configPrefix + SIGNATURE_SECRET);
                    if (signatureSecret == null) {
                        signatureSecret = Long.toString(new Random().nextLong());
                        log.warn("'signature.secret' configuration not set, using a random value as secret");
                    }
                    final byte[] secretBytes = StringUtils.toUtf8(signatureSecret);
                    SignerSecretProvider signerSecretProvider = new SignerSecretProvider() {
                        @Override
                        public void init(Properties config, ServletContext servletContext, long tokenValidity)
                                throws Exception {

                        }

                        @Override
                        public byte[] getCurrentSecret() {
                            return secretBytes;
                        }

                        @Override
                        public byte[][] getAllSecrets() {
                            return new byte[][] { secretBytes };
                        }
                    };
                    mySigner = new Signer(signerSecretProvider);
                } finally {
                    Thread.currentThread().setContextClassLoader(prevLoader);
                }
            }

            // Copied from hadoop-auth's AuthenticationFilter, to allow us to change error response handling in doFilterSuper
            @Override
            protected AuthenticationToken getToken(HttpServletRequest request)
                    throws IOException, AuthenticationException {
                AuthenticationToken token = null;
                String tokenStr = null;
                Cookie[] cookies = request.getCookies();
                if (cookies != null) {
                    for (Cookie cookie : cookies) {
                        if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
                            tokenStr = cookie.getValue();
                            try {
                                tokenStr = mySigner.verifyAndExtract(tokenStr);
                            } catch (SignerException ex) {
                                throw new AuthenticationException(ex);
                            }
                            break;
                        }
                    }
                }
                if (tokenStr != null) {
                    token = AuthenticationToken.parse(tokenStr);
                    if (!token.getType().equals(getAuthenticationHandler().getType())) {
                        throw new AuthenticationException("Invalid AuthenticationToken type");
                    }
                    if (token.isExpired()) {
                        throw new AuthenticationException("AuthenticationToken expired");
                    }
                }
                return token;
            }

            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                    throws IOException, ServletException {
                HttpServletRequest httpReq = (HttpServletRequest) request;

                // If there's already an auth result, then we have authenticated already, skip this.
                if (request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT) != null) {
                    filterChain.doFilter(request, response);
                    return;
                }

                if (loginContext == null) {
                    initializeKerberosLogin();
                }

                String path = ((HttpServletRequest) request).getRequestURI();
                if (isExcluded(path)) {
                    filterChain.doFilter(request, response);
                } else {
                    String clientPrincipal = null;
                    try {
                        Cookie[] cookies = httpReq.getCookies();
                        if (cookies == null) {
                            clientPrincipal = getPrincipalFromRequestNew((HttpServletRequest) request);
                        } else {
                            clientPrincipal = null;
                            for (Cookie cookie : cookies) {
                                if ("hadoop.auth".equals(cookie.getName())) {
                                    Matcher matcher = HADOOP_AUTH_COOKIE_REGEX.matcher(cookie.getValue());
                                    if (matcher.matches()) {
                                        clientPrincipal = matcher.group(1);
                                        break;
                                    }
                                }
                            }
                        }
                    } catch (Exception ex) {
                        clientPrincipal = null;
                    }

                    if (clientPrincipal != null) {
                        request.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT,
                                new AuthenticationResult(clientPrincipal, authorizerName, null));
                    }
                }

                doFilterSuper(request, response, filterChain);
            }

            // Copied from hadoop-auth's AuthenticationFilter, to allow us to change error response handling
            private void doFilterSuper(ServletRequest request, ServletResponse response, FilterChain filterChain)
                    throws IOException, ServletException {
                boolean unauthorizedResponse = true;
                int errCode = HttpServletResponse.SC_UNAUTHORIZED;
                AuthenticationException authenticationEx = null;
                HttpServletRequest httpRequest = (HttpServletRequest) request;
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                boolean isHttps = "https".equals(httpRequest.getScheme());
                try {
                    boolean newToken = false;
                    AuthenticationToken token;
                    try {
                        token = getToken(httpRequest);
                    } catch (AuthenticationException ex) {
                        log.warn("AuthenticationToken ignored: " + ex.getMessage());
                        // will be sent back in a 401 unless filter authenticates
                        authenticationEx = ex;
                        token = null;
                    }
                    if (getAuthenticationHandler().managementOperation(token, httpRequest, httpResponse)) {
                        if (token == null) {
                            if (log.isDebugEnabled()) {
                                log.debug("Request [{%s}] triggering authentication", getRequestURL(httpRequest));
                            }
                            token = getAuthenticationHandler().authenticate(httpRequest, httpResponse);
                            if (token != null && token.getExpires() != 0
                                    && token != AuthenticationToken.ANONYMOUS) {
                                token.setExpires(System.currentTimeMillis() + getValidity() * 1000);
                            }
                            newToken = true;
                        }
                        if (token != null) {
                            unauthorizedResponse = false;
                            if (log.isDebugEnabled()) {
                                log.debug("Request [{%s}] user [{%s}] authenticated", getRequestURL(httpRequest),
                                        token.getUserName());
                            }
                            final AuthenticationToken authToken = token;
                            httpRequest = new HttpServletRequestWrapper(httpRequest) {

                                @Override
                                public String getAuthType() {
                                    return authToken.getType();
                                }

                                @Override
                                public String getRemoteUser() {
                                    return authToken.getUserName();
                                }

                                @Override
                                public Principal getUserPrincipal() {
                                    return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null;
                                }
                            };
                            if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
                                String signedToken = mySigner.sign(token.toString());
                                createAuthCookie(httpResponse, signedToken, getCookieDomain(), getCookiePath(),
                                        token.getExpires(), isHttps);
                            }
                            doFilter(filterChain, httpRequest, httpResponse);
                        }
                    } else {
                        unauthorizedResponse = false;
                    }
                } catch (AuthenticationException ex) {
                    // exception from the filter itself is fatal
                    errCode = HttpServletResponse.SC_FORBIDDEN;
                    authenticationEx = ex;
                    if (log.isDebugEnabled()) {
                        log.debug("Authentication exception: " + ex.getMessage(), ex);
                    } else {
                        log.warn("Authentication exception: " + ex.getMessage());
                    }
                }
                if (unauthorizedResponse) {
                    if (!httpResponse.isCommitted()) {
                        createAuthCookie(httpResponse, "", getCookieDomain(), getCookiePath(), 0, isHttps);
                        // If response code is 401. Then WWW-Authenticate Header should be
                        // present.. reset to 403 if not found..
                        if ((errCode == HttpServletResponse.SC_UNAUTHORIZED) && (!httpResponse.containsHeader(
                                org.apache.hadoop.security.authentication.client.KerberosAuthenticator.WWW_AUTHENTICATE))) {
                            errCode = HttpServletResponse.SC_FORBIDDEN;
                        }
                        if (authenticationEx == null) {
                            // Don't send an error response here, unlike the base AuthenticationFilter implementation.
                            // This request did not use Kerberos auth.
                            // Instead, we will send an error response in PreResponseAuthorizationCheckFilter to allow
                            // other Authenticator implementations to check the request.
                            filterChain.doFilter(request, response);
                        } else {
                            // Do send an error response here, we attempted Kerberos authentication and failed.
                            httpResponse.sendError(errCode, authenticationEx.getMessage());
                        }
                    }
                }
            }
        };
    }

    @Override
    public Class<? extends Filter> getFilterClass() {
        return null;
    }

    @Override
    public Map<String, String> getInitParameters() {
        Map<String, String> params = new HashMap<String, String>();
        try {
            params.put("kerberos.principal", SecurityUtil.getServerPrincipal(serverPrincipal, node.getHost()));
            params.put("kerberos.keytab", serverKeytab);
            params.put(AuthenticationFilter.AUTH_TYPE, DruidKerberosAuthenticationHandler.class.getName());
            params.put("kerberos.name.rules", authToLocal);
            if (cookieSignatureSecret != null) {
                params.put("signature.secret", cookieSignatureSecret);
            }
        } catch (IOException e) {
            Throwables.propagate(e);
        }
        return params;
    }

    @Override
    public String getPath() {
        return "/*";
    }

    @Override
    public EnumSet<DispatcherType> getDispatcherType() {
        return null;
    }

    @Override
    public String getAuthChallengeHeader() {
        return "Negotiate";
    }

    @Override
    public AuthenticationResult authenticateJDBCContext(Map<String, Object> context) {
        throw new UnsupportedOperationException("JDBC Kerberos auth not supported yet");
    }

    @Override
    public HttpClient createEscalatedClient(HttpClient baseClient) {
        return new KerberosHttpClient(baseClient, internalClientPrincipal, internalClientKeytab);
    }

    @Override
    public org.eclipse.jetty.client.HttpClient createEscalatedJettyClient(
            org.eclipse.jetty.client.HttpClient baseClient) {
        baseClient.getAuthenticationStore().addAuthentication(new Authentication() {
            @Override
            public boolean matches(String type, URI uri, String realm) {
                return true;
            }

            @Override
            public Result authenticate(final Request request, ContentResponse response,
                    Authentication.HeaderInfo headerInfo, Attributes context) {
                return new Result() {
                    @Override
                    public URI getURI() {
                        return request.getURI();
                    }

                    @Override
                    public void apply(Request request) {
                        try {
                            // No need to set cookies as they are handled by Jetty Http Client itself.
                            URI uri = request.getURI();
                            if (DruidKerberosUtil.needToSendCredentials(baseClient.getCookieStore(), uri)) {
                                log.debug(
                                        "No Auth Cookie found for URI[%s]. Existing Cookies[%s] Authenticating... ",
                                        uri, baseClient.getCookieStore().getCookies());
                                final String host = request.getHost();
                                DruidKerberosUtil.authenticateIfRequired(internalClientPrincipal,
                                        internalClientKeytab);
                                UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
                                String challenge = currentUser.doAs(new PrivilegedExceptionAction<String>() {
                                    @Override
                                    public String run() throws Exception {
                                        return DruidKerberosUtil.kerberosChallenge(host);
                                    }
                                });
                                request.getHeaders().add(HttpHeaders.Names.AUTHORIZATION, "Negotiate " + challenge);
                            } else {
                                log.debug("Found Auth Cookie found for URI[%s].", uri);
                            }
                        } catch (Throwable e) {
                            Throwables.propagate(e);
                        }
                    }
                };
            }
        });
        return baseClient;
    }

    @Override
    public AuthenticationResult createEscalatedAuthenticationResult() {
        return new AuthenticationResult(internalClientPrincipal, authorizerName, null);
    }

    private boolean isExcluded(String path) {
        for (String excluded : excludedPaths) {
            if (path.startsWith(excluded)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Kerberos context configuration for the JDK GSS library. Copied from hadoop-auth's KerberosAuthenticationHandler.
     */
    public static class DruidKerberosConfiguration extends Configuration {
        private String keytab;
        private String principal;

        public DruidKerberosConfiguration(String keytab, String principal) {
            this.keytab = keytab;
            this.principal = principal;
        }

        @Override
        public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
            Map<String, String> options = new HashMap<String, String>();
            if (System.getProperty("java.vendor").contains("IBM")) {
                options.put("useKeytab", keytab.startsWith("file://") ? keytab : "file://" + keytab);
                options.put("principal", principal);
                options.put("credsType", "acceptor");
            } else {
                options.put("keyTab", keytab);
                options.put("principal", principal);
                options.put("useKeyTab", "true");
                options.put("storeKey", "true");
                options.put("doNotPrompt", "true");
                options.put("useTicketCache", "true");
                options.put("renewTGT", "true");
                options.put("isInitiator", "false");
            }
            options.put("refreshKrb5Config", "true");
            String ticketCache = System.getenv("KRB5CCNAME");
            if (ticketCache != null) {
                if (System.getProperty("java.vendor").contains("IBM")) {
                    options.put("useDefaultCcache", "true");
                    // The first value searched when "useDefaultCcache" is used.
                    System.setProperty("KRB5CCNAME", ticketCache);
                    options.put("renewTGT", "true");
                    options.put("credsType", "both");
                } else {
                    options.put("ticketCache", ticketCache);
                }
            }
            if (log.isDebugEnabled()) {
                options.put("debug", "true");
            }

            return new AppConfigurationEntry[] { new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(),
                    AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options), };
        }
    }

    private String getPrincipalFromRequestNew(HttpServletRequest req) {
        String authorization = req
                .getHeader(org.apache.hadoop.security.authentication.client.KerberosAuthenticator.AUTHORIZATION);
        if (authorization == null || !authorization
                .startsWith(org.apache.hadoop.security.authentication.client.KerberosAuthenticator.NEGOTIATE)) {
            return null;
        } else {
            authorization = authorization.substring(
                    org.apache.hadoop.security.authentication.client.KerberosAuthenticator.NEGOTIATE.length())
                    .trim();
            final Base64 base64 = new Base64(0);
            final byte[] clientToken = base64.decode(authorization);
            try {
                DerInputStream ticketStream = new DerInputStream(clientToken);
                DerValue[] values = ticketStream.getSet(clientToken.length, true);

                // see this link for AP-REQ format: https://tools.ietf.org/html/rfc1510#section-5.5.1
                for (DerValue value : values) {
                    if (isValueAPReq(value)) {
                        APReq apReq = new APReq(value);
                        Ticket ticket = apReq.ticket;
                        EncryptedData encData = ticket.encPart;
                        int eType = encData.getEType();

                        // find the server's key
                        EncryptionKey finalKey = null;
                        Subject serverSubj = loginContext.getSubject();
                        Set<Object> serverCreds = serverSubj.getPrivateCredentials(Object.class);
                        for (Object cred : serverCreds) {
                            if (cred instanceof KeyTab) {
                                KeyTab serverKeyTab = (KeyTab) cred;
                                KerberosPrincipal serverPrincipal = new KerberosPrincipal(this.serverPrincipal);
                                KerberosKey[] serverKeys = serverKeyTab.getKeys(serverPrincipal);
                                for (KerberosKey key : serverKeys) {
                                    if (key.getKeyType() == eType) {
                                        finalKey = new EncryptionKey(key.getKeyType(), key.getEncoded());
                                        break;
                                    }
                                }
                            }
                        }

                        if (finalKey == null) {
                            log.error("Could not find matching key from server creds.");
                            return null;
                        }

                        // decrypt the ticket with the server's key
                        byte[] decryptedBytes = encData.decrypt(finalKey, KeyUsage.KU_TICKET);
                        decryptedBytes = encData.reset(decryptedBytes);
                        EncTicketPart decrypted = new EncTicketPart(decryptedBytes);
                        String clientPrincipal = decrypted.cname.toString();
                        return clientPrincipal;
                    }
                }
            } catch (Exception ex) {
                Throwables.propagate(ex);
            }
        }

        return null;
    }

    private boolean isValueAPReq(DerValue value) {
        return value.isConstructed((byte) Krb5.KRB_AP_REQ);
    }

    private void initializeKerberosLogin() throws ServletException {
        String principal;
        String keytab;

        try {
            principal = SecurityUtil.getServerPrincipal(serverPrincipal, node.getHost());
            if (principal == null || principal.trim().length() == 0) {
                throw new ServletException("Principal not defined in configuration");
            }
            keytab = serverKeytab;
            if (keytab == null || keytab.trim().length() == 0) {
                throw new ServletException("Keytab not defined in configuration");
            }
            if (!new File(keytab).exists()) {
                throw new ServletException("Keytab does not exist: " + keytab);
            }

            Set<Principal> principals = new HashSet<Principal>();
            principals.add(new KerberosPrincipal(principal));
            Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>());

            DruidKerberosConfiguration kerberosConfiguration = new DruidKerberosConfiguration(keytab, principal);

            log.info("Login using keytab " + keytab + ", for principal " + principal);
            loginContext = new LoginContext("", subject, null, kerberosConfiguration);
            loginContext.login();

            log.info("Initialized, principal %s from keytab %s", principal, keytab);
        } catch (Exception ex) {
            throw new ServletException(ex);
        }
    }
}