init
This commit is contained in:
496
java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
Normal file
496
java/org/apache/catalina/authenticator/SpnegoAuthenticator.java
Normal file
@@ -0,0 +1,496 @@
|
||||
/*
|
||||
* 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 javax.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<GSSCredential> action =
|
||||
new PrivilegedExceptionAction<GSSCredential>() {
|
||||
@Override
|
||||
public GSSCredential run() throws GSSException {
|
||||
return 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<byte[]> {
|
||||
|
||||
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<Principal> {
|
||||
|
||||
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)
|
||||
* <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user