/*
* See LICENSE for licensing and NOTICE for copyright.
*/
package net.shibboleth.idp.cas.authn;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import net.shibboleth.idp.cas.config.ProxyGrantingTicketConfiguration;
import net.shibboleth.idp.profile.config.SecurityConfiguration;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.opensaml.security.SecurityException;
import org.opensaml.security.trust.TrustEngine;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.security.x509.X509Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Authenticates a proxy callback URL over SSL/TLS by performing a PKIX trust check using Apache HttpComponents.
*
* @author Marvin S. Addison
*/
public class PkixProxyAuthenticator extends AbstractProxyAuthenticator {
/**
* Delegates X.509 certificate trust to an underlying OpenSAML <code>TrustEngine</code>.
*/
private static class TrustEngineTrustStrategy implements TrustStrategy {
private final TrustEngine<X509Credential> trustEngine;
/** Class logger. */
private final Logger log = LoggerFactory.getLogger(TrustEngineTrustStrategy.class);
public TrustEngineTrustStrategy(@Nonnull final TrustEngine<X509Credential> engine) {
trustEngine = Constraint.isNotNull(engine, "TrustEngine cannot be null");
}
@Override
public boolean isTrusted(final X509Certificate[] certificates, final String authType)
throws CertificateException {
if (certificates == null || certificates.length < 1) {
return false;
}
// Assume the first certificate is the end-entity cert
try {
log.debug("Validating cert {} issued by {}",
certificates[0].getSubjectDN().getName(),
certificates[0].getIssuerDN().getName());
return trustEngine.validate(new BasicX509Credential(certificates[0]), new CriteriaSet());
} catch (SecurityException e) {
throw new CertificateException("X509 validation error", e);
}
}
}
/** Default connection and socket timeout in ms. */
private static final int DEFAULT_TIMEOUT = 800;
/** Class logger. */
private final Logger log = LoggerFactory.getLogger(PkixProxyAuthenticator.class);
private final SSLConnectionSocketFactory socketFactory;
/** Connection and socket timeout. */
@Positive private int timeout = DEFAULT_TIMEOUT;
/**
* Creates a new instance.
*
* @param x509TrustEngine X.509 trust engine used to validate the TLS certificate presented by the proxy
* callback endpoint.
*/
public PkixProxyAuthenticator(@Nonnull TrustEngine<X509Credential> x509TrustEngine) {
Constraint.isNotNull(x509TrustEngine, "Trust engine cannot be null");
try {
SSLContext sslContext = SSLContexts.custom()
.useTLS()
.loadTrustMaterial(null, new TrustEngineTrustStrategy(x509TrustEngine))
.build();
socketFactory = new SSLConnectionSocketFactory(
sslContext,
SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
} catch (Exception e) {
throw new RuntimeException("SSL initialization error", e);
}
}
/**
* Sets connect and socket timeouts for HTTP connection to proxy callback endpoint.
*
* @param timeout Non-zero timeout in milliseconds for both connection and socket timeouts.
*/
public void setTimeout(@Positive final int timeout) {
this.timeout = (int) Constraint.isGreaterThan(timeout, 0, "Timeout must be positive");
}
@Override
protected int authenticateProxyCallback(final URI callbackUri) throws GeneralSecurityException {
CloseableHttpClient httpClient = null;
CloseableHttpResponse response = null;
try {
httpClient = createHttpClient();
log.debug("Attempting to connect to {}", callbackUri);
final HttpGet request = new HttpGet(callbackUri);
request.setConfig(
RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.build());
response = httpClient.execute(request);
return response.getStatusLine().getStatusCode();
} catch (ClientProtocolException e) {
throw new RuntimeException("HTTP protocol error", e);
} catch (SSLException e) {
if (e.getCause() instanceof CertificateException) {
throw (CertificateException) e.getCause();
}
throw new GeneralSecurityException("SSL connection error", e);
} catch (IOException e) {
throw new RuntimeException("IO error", e);
} finally {
close(response);
close(httpClient);
}
}
private CloseableHttpClient createHttpClient() {
final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register(HTTPS_SCHEME, socketFactory).build();
final BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(registry);
return HttpClients.custom().setConnectionManager(connectionManager).build();
}
private void close(Closeable resource) {
if (resource != null) {
try {
resource.close();
} catch (IOException e) {
log.warn("Error closing " + resource, e);
}
}
}
}