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

org.apache.catalina.authenticator.SpnegoAuthenticator Maven / Gradle / Ivy

There is a newer version: 11.0.0-M19
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.catalina.authenticator;

import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.LinkedHashMap;
import java.util.regex.Pattern;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.Realm;
import org.apache.catalina.connector.Request;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.codec.binary.Base64;
import org.apache.tomcat.util.compat.JreVendor;
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;

/**
 * A SPNEGO authenticator that uses the SPNEGO/Kerberos support built in to Java
 * 6. Successful Kerberos authentication depends on the correct configuration of
 * multiple components. If the configuration is invalid, the error messages are
 * often cryptic although a Google search will usually point you in the right
 * direction.
 */
public class SpnegoAuthenticator extends AuthenticatorBase {

    private final Log log = LogFactory.getLog(SpnegoAuthenticator.class); // must not be static
    private static final String AUTH_HEADER_VALUE_NEGOTIATE = "Negotiate";

    private String loginConfigName = Constants.DEFAULT_LOGIN_MODULE_NAME;
    public String getLoginConfigName() {
        return loginConfigName;
    }
    public void setLoginConfigName(String loginConfigName) {
        this.loginConfigName = loginConfigName;
    }

    private boolean storeDelegatedCredential = true;
    public boolean isStoreDelegatedCredential() {
        return storeDelegatedCredential;
    }
    public void setStoreDelegatedCredential(
            boolean storeDelegatedCredential) {
        this.storeDelegatedCredential = storeDelegatedCredential;
    }

    private Pattern noKeepAliveUserAgents = null;
    public String getNoKeepAliveUserAgents() {
        Pattern p = noKeepAliveUserAgents;
        if (p == null) {
            return null;
        } else {
            return p.pattern();
        }
    }
    public void setNoKeepAliveUserAgents(String noKeepAliveUserAgents) {
        if (noKeepAliveUserAgents == null ||
                noKeepAliveUserAgents.length() == 0) {
            this.noKeepAliveUserAgents = null;
        } else {
            this.noKeepAliveUserAgents = Pattern.compile(noKeepAliveUserAgents);
        }
    }

    private boolean applyJava8u40Fix = true;
    public boolean getApplyJava8u40Fix() {
        return applyJava8u40Fix;
    }
    public void setApplyJava8u40Fix(boolean applyJava8u40Fix) {
        this.applyJava8u40Fix = applyJava8u40Fix;
    }


    @Override
    protected String getAuthMethod() {
        return Constants.SPNEGO_METHOD;
    }


    @Override
    protected void initInternal() throws LifecycleException {
        super.initInternal();

        // Kerberos configuration file location
        String krb5Conf = System.getProperty(Constants.KRB5_CONF_PROPERTY);
        if (krb5Conf == null) {
            // System property not set, use the Tomcat default
            File krb5ConfFile = new File(container.getCatalinaBase(),
                    Constants.DEFAULT_KRB5_CONF);
            System.setProperty(Constants.KRB5_CONF_PROPERTY,
                    krb5ConfFile.getAbsolutePath());
        }

        // JAAS configuration file location
        String jaasConf = System.getProperty(Constants.JAAS_CONF_PROPERTY);
        if (jaasConf == null) {
            // System property not set, use the Tomcat default
            File jaasConfFile = new File(container.getCatalinaBase(),
                    Constants.DEFAULT_JAAS_CONF);
            System.setProperty(Constants.JAAS_CONF_PROPERTY,
                    jaasConfFile.getAbsolutePath());
        }
    }


    @Override
    protected boolean doAuthenticate(Request request, HttpServletResponse response)
            throws IOException {

        if (checkForCachedAuthentication(request, response, true)) {
            return true;
        }

        MessageBytes authorization =
            request.getCoyoteRequest().getMimeHeaders()
            .getValue("authorization");

        if (authorization == null) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("authenticator.noAuthHeader"));
            }
            response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        authorization.toBytes();
        ByteChunk authorizationBC = authorization.getByteChunk();

        if (!authorizationBC.startsWithIgnoreCase("negotiate ", 0)) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString(
                        "spnegoAuthenticator.authHeaderNotNego"));
            }
            response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        authorizationBC.setOffset(authorizationBC.getOffset() + 10);

        byte[] decoded = Base64.decodeBase64(authorizationBC.getBuffer(),
                authorizationBC.getOffset(),
                authorizationBC.getLength());

        if (getApplyJava8u40Fix()) {
            SpnegoTokenFixer.fix(decoded);
        }

        if (decoded.length == 0) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString(
                        "spnegoAuthenticator.authHeaderNoToken"));
            }
            response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        }

        LoginContext lc = null;
        GSSContext gssContext = null;
        byte[] outToken = null;
        Principal principal = null;
        try {
            try {
                lc = new LoginContext(getLoginConfigName());
                lc.login();
            } catch (LoginException e) {
                log.error(sm.getString("spnegoAuthenticator.serviceLoginFail"),
                        e);
                response.sendError(
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return false;
            }

            Subject subject = lc.getSubject();

            // Assume the GSSContext is stateless
            // TODO: Confirm this assumption
            final GSSManager manager = GSSManager.getInstance();
            // IBM JDK only understands indefinite lifetime
            final int credentialLifetime;
            if (JreVendor.IS_IBM_JVM) {
                credentialLifetime = GSSCredential.INDEFINITE_LIFETIME;
            } else {
                credentialLifetime = GSSCredential.DEFAULT_LIFETIME;
            }
            final PrivilegedExceptionAction action =
                    () -> manager.createCredential(null,
                            credentialLifetime,
                            new Oid("1.3.6.1.5.5.2"),
                            GSSCredential.ACCEPT_ONLY);
            gssContext = manager.createContext(Subject.doAs(subject, action));

            outToken = Subject.doAs(lc.getSubject(), new AcceptAction(gssContext, decoded));

            if (outToken == null) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString(
                            "spnegoAuthenticator.ticketValidateFail"));
                }
                // Start again
                response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }

            principal = Subject.doAs(subject, new AuthenticateAction(
                    context.getRealm(), gssContext, storeDelegatedCredential));

        } catch (GSSException e) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("spnegoAuthenticator.ticketValidateFail"), e);
            }
            response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        } catch (PrivilegedActionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof GSSException) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("spnegoAuthenticator.serviceLoginFail"), e);
                }
            } else {
                log.error(sm.getString("spnegoAuthenticator.serviceLoginFail"), e);
            }
            response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return false;
        } finally {
            if (gssContext != null) {
                try {
                    gssContext.dispose();
                } catch (GSSException e) {
                    // Ignore
                }
            }
            if (lc != null) {
                try {
                    lc.logout();
                } catch (LoginException e) {
                    // Ignore
                }
            }
        }

        // Send response token on success and failure
        response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE + " "
                + Base64.encodeBase64String(outToken));

        if (principal != null) {
            register(request, response, principal, Constants.SPNEGO_METHOD,
                    principal.getName(), null);

            Pattern p = noKeepAliveUserAgents;
            if (p != null) {
                MessageBytes ua =
                        request.getCoyoteRequest().getMimeHeaders().getValue(
                                "user-agent");
                if (ua != null && p.matcher(ua.toString()).matches()) {
                    response.setHeader("Connection", "close");
                }
            }
            return true;
        }

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        return false;
    }


    /**
     * This class gets a gss credential via a privileged action.
     */
    public static class AcceptAction implements PrivilegedExceptionAction {

        GSSContext gssContext;

        byte[] decoded;

        public AcceptAction(GSSContext context, byte[] decodedToken) {
            this.gssContext = context;
            this.decoded = decodedToken;
        }

        @Override
        public byte[] run() throws GSSException {
            return gssContext.acceptSecContext(decoded,
                    0, decoded.length);
        }
    }


    public static class AuthenticateAction implements PrivilegedAction {

        private final Realm realm;
        private final GSSContext gssContext;
        private final boolean storeDelegatedCredential;

        public AuthenticateAction(Realm realm, GSSContext gssContext,
                boolean storeDelegatedCredential) {
            this.realm = realm;
            this.gssContext = gssContext;
            this.storeDelegatedCredential = storeDelegatedCredential;
        }

        @Override
        public Principal run() {
            return realm.authenticate(gssContext, storeDelegatedCredential);
        }
    }


    /**
     * This class implements a hack around an incompatibility between the
     * SPNEGO implementation in Windows and the SPNEGO implementation in Java 8
     * update 40 onwards. It was introduced by the change to fix this bug:
     * https://bugs.openjdk.java.net/browse/JDK-8048194
     * (note: the change applied is not the one suggested in the bug report)
     * 

* It is not clear to me if Windows, Java or Tomcat is at fault here. I * think it is Java but I could be wrong. *

* This hack works by re-ordering the list of mechTypes in the NegTokenInit * token. */ public static class SpnegoTokenFixer { public static void fix(byte[] token) { SpnegoTokenFixer fixer = new SpnegoTokenFixer(token); fixer.fix(); } private final byte[] token; private int pos = 0; private SpnegoTokenFixer(byte[] token) { this.token = token; } // Fixes the token in-place private void fix() { /* * Useful references: * http://tools.ietf.org/html/rfc4121#page-5 * http://tools.ietf.org/html/rfc2743#page-81 * https://msdn.microsoft.com/en-us/library/ms995330.aspx */ // Scan until we find the mech types list. If we find anything // unexpected, abort the fix process. if (!tag(0x60)) { return; } if (!length()) { return; } if (!oid("1.3.6.1.5.5.2")) { return; } if (!tag(0xa0)) { return; } if (!length()) { return; } if (!tag(0x30)) { return; } if (!length()) { return; } if (!tag(0xa0)) { return; } lengthAsInt(); if (!tag(0x30)) { return; } // Now at the start of the mechType list. // Read the mechTypes into an ordered set int mechTypesLen = lengthAsInt(); int mechTypesStart = pos; LinkedHashMap mechTypeEntries = new LinkedHashMap<>(); while (pos < mechTypesStart + mechTypesLen) { int[] value = new int[2]; value[0] = pos; String key = oidAsString(); value[1] = pos - value[0]; mechTypeEntries.put(key, value); } // Now construct the re-ordered mechType list byte[] replacement = new byte[mechTypesLen]; int replacementPos = 0; int[] first = mechTypeEntries.remove("1.2.840.113554.1.2.2"); if (first != null) { System.arraycopy(token, first[0], replacement, replacementPos, first[1]); replacementPos += first[1]; } for (int[] markers : mechTypeEntries.values()) { System.arraycopy(token, markers[0], replacement, replacementPos, markers[1]); replacementPos += markers[1]; } // Finally, replace the original mechType list with the re-ordered // one. System.arraycopy(replacement, 0, token, mechTypesStart, mechTypesLen); } private boolean tag(int expected) { return (token[pos++] & 0xFF) == expected; } private boolean length() { // No need to retain the length - just need to consume it and make // sure it is valid. int len = lengthAsInt(); return pos + len == token.length; } private int lengthAsInt() { int len = token[pos++] & 0xFF; if (len > 127) { int bytes = len - 128; len = 0; for (int i = 0; i < bytes; i++) { len = len << 8; len = len + (token[pos++] & 0xff); } } return len; } private boolean oid(String expected) { return expected.equals(oidAsString()); } private String oidAsString() { if (!tag(0x06)) { return null; } StringBuilder result = new StringBuilder(); int len = lengthAsInt(); // First byte is special case int v = token[pos++] & 0xFF; int c2 = v % 40; int c1 = (v - c2) / 40; result.append(c1); result.append('.'); result.append(c2); int c = 0; boolean write = false; for (int i = 1; i < len; i++) { int b = token[pos++] & 0xFF; if (b > 127) { b -= 128; } else { write = true; } c = c << 7; c += b; if (write) { result.append('.'); result.append(c); c = 0; write = false; } } return result.toString(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy