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:
parent
2549df99ce
commit
68790d861c
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue