| /* |
| * 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.util.Base64; |
| import java.util.LinkedHashMap; |
| import java.util.concurrent.CompletionException; |
| 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.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.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.isEmpty()) { |
| 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.setStart(authorizationBC.getStart() + 10); |
| |
| byte[] encoded = new byte[authorizationBC.getLength()]; |
| System.arraycopy(authorizationBC.getBuffer(), authorizationBC.getStart(), encoded, 0, |
| authorizationBC.getLength()); |
| byte[] decoded = Base64.getDecoder().decode(encoded); |
| |
| 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; |
| Principal principal; |
| 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; |
| } |
| gssContext = manager.createContext(Subject.callAs(subject, () -> manager.createCredential(null, |
| credentialLifetime, new Oid("1.3.6.1.5.5.2"), GSSCredential.ACCEPT_ONLY))); |
| |
| final GSSContext gssContextFinal = gssContext; |
| outToken = Subject.callAs(subject, () -> gssContextFinal.acceptSecContext(decoded, 0, decoded.length)); |
| |
| 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.callAs(subject, |
| () -> context.getRealm().authenticate(gssContextFinal, 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 (CompletionException 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.getEncoder().encodeToString(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; |
| } |
| |
| |
| @Override |
| protected boolean isPreemptiveAuthPossible(Request request) { |
| MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); |
| return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("negotiate ", 0); |
| } |
| |
| |
| /** |
| * 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: |
| * <a href="https://bugs.openjdk.java.net/browse/JDK-8048194">JDK-8048194</a> (note: the change applied is not the |
| * one suggested in the bug report) |
| * <p> |
| * 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. |
| * <p> |
| * 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<String,int[]> 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(); |
| } |
| } |
| } |