Issue #5933 ClientCertAuthenticator is not using SslContextFactory (#5934)

Added SslClientCertAuthenticator
Co-authored-by: olivier lamy <oliver.lamy@gmail.com>
Signed-off-by: Greg Wilkins <gregw@webtide.com>

Co-authored-by: gregw <gregw@webtide.com>
This commit is contained in:
Olivier Lamy 2021-02-10 11:03:54 +10:00 committed by GitHub
parent 2549df99ce
commit 68790d861c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 414 additions and 1 deletions

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.security;
import java.util.Collection;
import javax.servlet.ServletContext;
import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
@ -21,8 +22,12 @@ import org.eclipse.jetty.security.authentication.ClientCertAuthenticator;
import org.eclipse.jetty.security.authentication.ConfigurableSpnegoAuthenticator;
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.security.authentication.SslClientCertAuthenticator;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Default Authenticator Factory.
@ -31,6 +36,7 @@ import org.eclipse.jetty.util.security.Constraint;
* <li>{@link org.eclipse.jetty.security.authentication.DigestAuthenticator}</li>
* <li>{@link org.eclipse.jetty.security.authentication.FormAuthenticator}</li>
* <li>{@link org.eclipse.jetty.security.authentication.ClientCertAuthenticator}</li>
* <li>{@link SslClientCertAuthenticator}</li>
* </ul>
* All authenticators derived from {@link org.eclipse.jetty.security.authentication.LoginAuthenticator} are
* wrapped with a {@link org.eclipse.jetty.security.authentication.DeferredAuthentication}
@ -45,6 +51,9 @@ import org.eclipse.jetty.util.security.Constraint;
*/
public class DefaultAuthenticatorFactory implements Authenticator.Factory
{
private static final Logger LOG = LoggerFactory.getLogger(DefaultAuthenticatorFactory.class);
LoginService _loginService;
@Override
@ -64,7 +73,25 @@ public class DefaultAuthenticatorFactory implements Authenticator.Factory
else if (Constraint.__NEGOTIATE_AUTH.equalsIgnoreCase(auth)) // see Bug #377076
authenticator = new ConfigurableSpnegoAuthenticator(Constraint.__NEGOTIATE_AUTH);
if (Constraint.__CERT_AUTH.equalsIgnoreCase(auth) || Constraint.__CERT_AUTH2.equalsIgnoreCase(auth))
authenticator = new ClientCertAuthenticator();
{
Collection<SslContextFactory> sslContextFactories = server.getBeans(SslContextFactory.class);
if (sslContextFactories.size() != 1)
{
if (sslContextFactories.size() > 1)
{
LOG.info("Multiple SslContextFactory instances discovered. Directly configure a SslClientCertAuthenticator to use one.");
}
else
{
LOG.debug("No SslContextFactory instances discovered. Directly configure a SslClientCertAuthenticator to use one.");
}
authenticator = new ClientCertAuthenticator();
}
else
{
authenticator = new SslClientCertAuthenticator(sslContextFactories.iterator().next());
}
}
return authenticator;
}

View File

@ -35,6 +35,10 @@ import org.eclipse.jetty.util.security.CertificateValidator;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
@Deprecated
/**
* @deprecated Prefer using {@link SslClientCertAuthenticator}
*/
public class ClientCertAuthenticator extends LoginAuthenticator
{
/**

View File

@ -0,0 +1,153 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.security.authentication;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Objects;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* CLIENT-CERT authenticator.
*
* <p>This {@link org.eclipse.jetty.security.Authenticator} implements client certificate authentication.
* The client certificates available in the request will be verified against the configured {@link SslContextFactory} instance
* </p>
*/
public class SslClientCertAuthenticator
extends LoginAuthenticator
{
/**
* Set to true if SSL certificate validation is not required
* per default it's true as this is the goal of this implementation
*/
private boolean validateCerts = true;
private SslContextFactory sslContextFactory;
public SslClientCertAuthenticator(SslContextFactory sslContextFactory)
{
super();
Objects.nonNull(sslContextFactory);
this.sslContextFactory = sslContextFactory;
}
@Override
public String getAuthMethod()
{
return Constraint.__CERT_AUTH;
}
@Override
public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
{
if (!mandatory)
return new DeferredAuthentication(this);
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
X509Certificate[] certs = (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate");
try
{
// Need certificates.
if (certs != null && certs.length > 0)
{
if (validateCerts)
{
sslContextFactory.validateCerts(certs);
}
for (X509Certificate cert : certs)
{
if (cert == null)
continue;
Principal principal = cert.getSubjectDN();
if (principal == null)
principal = cert.getIssuerDN();
final String username = principal == null ? "clientcert" : principal.getName();
UserIdentity user = login(username, "", req);
if (user != null)
{
return new UserAuthentication(getAuthMethod(), user);
}
// try with null password
user = login(username, null, req);
if (user != null)
{
return new UserAuthentication(getAuthMethod(), user);
}
// try with certs sig against login service as previous behaviour
final char[] credential = Base64.getEncoder().encodeToString(cert.getSignature()).toCharArray();
user = login(username, credential, req);
if (user != null)
{
return new UserAuthentication(getAuthMethod(), user);
}
}
}
if (!DeferredAuthentication.isDeferred(response))
{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return Authentication.SEND_FAILURE;
}
return Authentication.UNAUTHENTICATED;
}
catch (Exception e)
{
throw new ServerAuthException(e.getMessage());
}
}
@Override
public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
{
return true;
}
/**
* @return true if SSL certificate has to be validated
*/
public boolean isValidateCerts()
{
return validateCerts;
}
/**
* @param validateCerts true if SSL certificates have to be validated
*/
public void setValidateCerts(boolean validateCerts)
{
validateCerts = validateCerts;
}
}

View File

@ -0,0 +1,193 @@
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.security;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
public class ClientCertAuthenticatorTest
{
private Server server;
private URI serverHttpsUri;
private URI serverHttpUri;
private HostnameVerifier origVerifier;
private SSLSocketFactory origSsf;
private static final String MESSAGE = "Yep CLIENT-CERT works";
@BeforeEach
public void setup() throws Exception
{
origSsf = HttpsURLConnection.getDefaultSSLSocketFactory();
origVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
server = new Server();
int port = 32080;
int securePort = 32443;
SslContextFactory.Server sslContextFactory = createServerSslContextFactory("cacerts.jks", "changeit");
// Setup HTTP Configuration
HttpConfiguration httpConf = new HttpConfiguration();
httpConf.setSecurePort(securePort);
httpConf.setSecureScheme("https");
ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConf));
httpConnector.setName("unsecured");
httpConnector.setPort(port);
// Setup HTTPS Configuration
HttpConfiguration httpsConf = new HttpConfiguration(httpConf);
SecureRequestCustomizer secureRequestCustomizer = new SecureRequestCustomizer();
secureRequestCustomizer.setSniRequired(false);
secureRequestCustomizer.setSniHostCheck(false);
httpsConf.addCustomizer(secureRequestCustomizer);
ServerConnector httpsConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpsConf));
httpsConnector.setName("secured");
httpsConnector.setPort(securePort);
// Add connectors
server.setConnectors(new Connector[]{httpConnector, httpsConnector});
ConstraintSecurityHandler constraintSecurityHandler = new ConstraintSecurityHandler();
constraintSecurityHandler.setAuthMethod(Constraint.__CERT_AUTH2);
ConstraintMapping constraintMapping = new ConstraintMapping();
Constraint constraint = new Constraint();
constraint.setName(Constraint.__CERT_AUTH2);
constraint.setRoles(new String[]{"Administrator"});
constraint.setAuthenticate(true);
constraintMapping.setConstraint(constraint);
constraintMapping.setMethod("GET");
constraintMapping.setPathSpec("/");
constraintSecurityHandler.addConstraintMapping(constraintMapping);
HashLoginService loginService = new HashLoginService();
constraintSecurityHandler.setLoginService(loginService);
loginService.setConfig("src/test/resources/realm.properties");
constraintSecurityHandler.setHandler(new FooHandler());
server.setHandler(constraintSecurityHandler);
server.addBean(sslContextFactory);
server.start();
// calculate serverUri
String host = httpConnector.getHost();
if (host == null)
{
host = "localhost";
}
serverHttpsUri = new URI(String.format("https://%s:%d/", host, httpsConnector.getLocalPort()));
serverHttpUri = new URI(String.format("http://%s:%d/", host, httpConnector.getLocalPort()));
}
@AfterEach
public void stopServer() throws Exception
{
if (origVerifier != null)
{
HttpsURLConnection.setDefaultHostnameVerifier(origVerifier);
}
if (origSsf != null)
{
HttpsURLConnection.setDefaultSSLSocketFactory(origSsf);
}
server.stop();
}
private SslContextFactory.Server createServerSslContextFactory(String trustStorePath, String trustStorePassword)
{
SslContextFactory.Server cf = new SslContextFactory.Server();
cf.setNeedClientAuth(true);
cf.setTrustStorePassword(trustStorePassword);
cf.setTrustStoreResource(Resource.newResource(MavenTestingUtils.getTestResourcePath(trustStorePath)));
cf.setKeyStoreResource(Resource.newResource(MavenTestingUtils.getTestResourcePath("clientcert.jks")));
cf.setKeyStorePassword("changeit");
cf.setSniRequired(false);
cf.setWantClientAuth(true);
return cf;
}
@Test
public void authzPass() throws Exception
{
HttpsURLConnection.setDefaultHostnameVerifier((s,sslSession) -> true);
SslContextFactory.Server cf = createServerSslContextFactory("cacerts.jks", "changeit");
cf.start();
HttpsURLConnection.setDefaultSSLSocketFactory(cf.getSslContext().getSocketFactory());
URL url = serverHttpsUri.resolve("/").toURL();
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
assertThat("response code", connection.getResponseCode(), is(200));
String response = IO.toString(connection.getInputStream());
assertThat("response message", response, containsString(MESSAGE));
}
@Test
public void authzNotPass() throws Exception
{
URL url = serverHttpUri.resolve("/").toURL();
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
assertThat("response code", connection.getResponseCode(), is(403));
}
static class FooHandler extends AbstractHandler
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
response.setContentType("text/plain; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
out.println(MESSAGE);
baseRequest.setHandled(true);
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,6 @@
# Setup default logging implementation for during testing
# Jetty Logging using jetty-slf4j-impl
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.security.LEVEL=DEBUG
#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
#org.eclipse.jetty.util.PathWatcher.Noisy.LEVEL=OFF

View File

@ -0,0 +1,27 @@
#
# This file defines users passwords and roles for a HashUserRealm
#
# The format is
# <username>: <password>[,<rolename> ...]
#
# Passwords may be clear text, obfuscated or checksummed. The class
# org.eclipse.util.Password should be used to generate obfuscated
# passwords or password checksums
#
# If DIGEST Authentication is used, the password must be in a recoverable
# format, either plain text or OBF:.
#
jetty: MD5:164c88b302622e17050af52c89945d44,user
admin: CRYPT:adpexzg3FUZAk,server-administrator,content-administrator,admin,user
other: OBF:1xmk1w261u9r1w1c1xmq,user
plain: plain,user
user: password,user
# This entry is for digest auth. The credential is a MD5 hash of username:realmname:password
digest: MD5:6e120743ad67abfbc385bc2bb754e297,user
j2ee: j2ee,Administrator,Employee
javajoe: javajoe,VP,Manager
# CN=CTS, OU=Java Software, O=Sun Microsystems Inc., L=Burlington, ST=MA, C=US
CN\=CTS,\ OU\=Java\ Software,\ O\=Sun\ Microsystems\ Inc.,\ L\=Burlington,\ ST\=MA,\ C\=US=,,Administrator

View File

@ -2001,6 +2001,14 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
return 0;
}
public void validateCerts(X509Certificate[] certs) throws Exception
{
KeyStore trustStore = loadTrustStore(_trustStoreResource);
Collection<? extends CRL> crls = loadCRL(_crlPath);
CertificateValidator validator = new CertificateValidator(trustStore, crls);
validator.validate(certs);
}
@Override
public String toString()
{