CASC-180 - Add support for Client Side Certificates

In order to utilize client side certificates, this commit facilitates the creation of a SSLSocketFactory on HttpsURLConnection for the client. The configuration is encapsulated inside a url factory instance that applies the adjustments where necessary.

This commit is continuation of the posted pending pull on github that is at:
https://github.com/Jasig/java-cas-client/pull/26

...and applies the suggestions and fixes that were brought to light during the code review.
This commit is contained in:
Misagh Moayyed 2013-01-24 12:01:47 -07:00
parent cd4dcc9fc9
commit b54cd179e2
12 changed files with 269 additions and 56 deletions

View File

@ -29,20 +29,19 @@ import java.net.URLEncoder;
/**
* Implementation of a ProxyRetriever that follows the CAS 2.0 specification.
* For more information on the CAS 2.0 specification, please see the <a
* href="http://www.ja-sig.org/products/cas/overview/protocol/index.html">specification
* href="http://www.jasig.org/cas/protocol">specification
* document</a>.
* <p/>
* In general, this class will make a call to the CAS server with some specified
* parameters and receive an XML response to parse.
*
* @author Scott Battaglia
* @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $
* @since 3.0
*/
public final class Cas20ProxyRetriever implements ProxyRetriever {
/** Unique Id for serialization. */
private static final long serialVersionUID = 560409469568911791L;
private static final long serialVersionUID = 560409469568911791L;
/**
* Instance of Commons Logging.

View File

@ -24,20 +24,20 @@ import org.jasig.cas.client.validation.ProxyListEditor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.URL;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
@ -323,28 +323,16 @@ public final class CommonUtils {
* Contacts the remote URL and returns the response.
*
* @param constructedUrl the url to contact.
* @param factory connection factory to prepare the URL connection instance
* @param encoding the encoding to use.
* @return the response.
*/
public static String getResponseFromServer(final URL constructedUrl, final String encoding) {
return getResponseFromServer(constructedUrl, HttpsURLConnection.getDefaultHostnameVerifier(), encoding);
}
public static String getResponseFromServer(final URL constructedUrl, final URLConnectionFactory factory, final String encoding) {
/**
* Contacts the remote URL and returns the response.
*
* @param constructedUrl the url to contact.
* @param hostnameVerifier Host name verifier to use for HTTPS connections.
* @param encoding the encoding to use.
* @return the response.
*/
public static String getResponseFromServer(final URL constructedUrl, final HostnameVerifier hostnameVerifier, final String encoding) {
URLConnection conn = null;
try {
conn = constructedUrl.openConnection();
if (conn instanceof HttpsURLConnection) {
((HttpsURLConnection)conn).setHostnameVerifier(hostnameVerifier);
}
conn = factory.getURLConnection(constructedUrl.openConnection());
final BufferedReader in;
if (CommonUtils.isEmpty(encoding)) {
@ -371,6 +359,7 @@ public final class CommonUtils {
}
}
/**
* Contacts the remote URL and returns the response.
*
@ -380,12 +369,12 @@ public final class CommonUtils {
*/
public static String getResponseFromServer(final String url, String encoding) {
try {
return getResponseFromServer(new URL(url), encoding);
return getResponseFromServer(new URL(url), new HttpsURLConnectionFactory(), encoding);
} catch (final MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public static ProxyList createProxyList(final String proxies) {
if (CommonUtils.isBlank(proxies)) {
return new ProxyList();
@ -410,4 +399,19 @@ public final class CommonUtils {
}
}
/**
* Unconditionally close a {@link Closeable}. Equivalent to {@link java.io.Closeable#close()}close(), except any exceptions
* will be ignored. This is typically used in finally blocks.
* @param resource
*/
public static void closeQuietly(final Closeable resource) {
try {
if (resource != null) {
resource.close();
}
} catch (final IOException e) {
//ignore
}
}
}

View File

@ -0,0 +1,126 @@
package org.jasig.cas.client.util;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URLConnection;
import java.security.KeyStore;
import java.util.Properties;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An implementation of the {@link URLConnectionFactory} whose responsible to configure
* the underlying <i>https</i> connection, if needed, with a given hostname and SSL socket factory based on the
* configuration provided.
*
* @author Misagh Moayyed
* @since 3.3
* @see #setHostnameVerifier(HostnameVerifier)
* @see #setSSLConfiguration(Properties)
*/
public final class HttpsURLConnectionFactory implements URLConnectionFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpsURLConnectionFactory.class);
/**
* Hostname verifier used when making an SSL request to the CAS server.
* Defaults to {@link HttpsURLConnection#getDefaultHostnameVerifier()}
*/
private HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
/**
* Properties file that can contains key/trust info for Client Side Certificates
*/
private Properties sslConfiguration = new Properties();
public HttpsURLConnectionFactory() {}
public HttpsURLConnectionFactory(final HostnameVerifier verifier, final Properties config) {
setHostnameVerifier(verifier);
setSSLConfiguration(config);
}
public final void setSSLConfiguration(final Properties config) {
this.sslConfiguration = config;
}
public final void setHostnameVerifier(final HostnameVerifier verifier) {
this.hostnameVerifier = verifier;
}
public URLConnection getURLConnection(final URLConnection url) {
return this.configureHttpsConnectionIfNeeded(url);
}
/**
* Configures the connection with specific settings for secure http connections
* If the connection instance is not a {@link HttpsURLConnection},
* no additional changes will be made and the connection itself is simply returned.
*
* @param conn the http connection
*/
private URLConnection configureHttpsConnectionIfNeeded(final URLConnection conn) {
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsConnection = (HttpsURLConnection) conn;
final SSLSocketFactory socketFactory = this.createSSLSocketFactory();
if (socketFactory != null) {
httpsConnection.setSSLSocketFactory(socketFactory);
}
if (this.hostnameVerifier != null) {
httpsConnection.setHostnameVerifier(this.hostnameVerifier);
}
}
return conn;
}
/**
* Creates a {@link SSLSocketFactory} based on the configuration specified
* <p>
* Sample properties file:
* <pre>
* protocol=TLS
* keyStoreType=JKS
* keyStorePath=/var/secure/location/.keystore
* keyStorePass=changeit
* certificatePassword=aGoodPass
* </pre>
* @param sslConfig {@link Properties}
* @return the {@link SSLSocketFactory}
*/
private SSLSocketFactory createSSLSocketFactory() {
InputStream keyStoreIS = null;
try {
final SSLContext sslContext = SSLContext.getInstance(this.sslConfiguration.getProperty("protocol", "SSL"));
if (this.sslConfiguration.getProperty("keyStoreType") != null) {
final KeyStore keyStore = KeyStore.getInstance(this.sslConfiguration.getProperty("keyStoreType"));
if (this.sslConfiguration.getProperty("keyStorePath") != null) {
keyStoreIS = new FileInputStream(this.sslConfiguration.getProperty("keyStorePath"));
if (this.sslConfiguration.getProperty("keyStorePass") != null) {
keyStore.load(keyStoreIS, this.sslConfiguration.getProperty("keyStorePass").toCharArray());
LOGGER.debug("Keystore has {} keys", keyStore.size());
final KeyManagerFactory keyManager = KeyManagerFactory.getInstance(this.sslConfiguration.getProperty("keyManagerType", "SunX509"));
keyManager.init(keyStore, this.sslConfiguration.getProperty("certificatePassword").toCharArray());
sslContext.init(keyManager.getKeyManagers(), null, null);
}
}
}
return sslContext.getSocketFactory();
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
} finally {
CommonUtils.closeQuietly(keyStoreIS);
}
return null;
}
}

View File

@ -0,0 +1,44 @@
/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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 the following location:
*
* 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.jasig.cas.client.util;
import java.net.URL;
import java.net.URLConnection;
/**
* A factory to prepare and configure {@link java.net.URLConnection} instances.
*
* @author Misagh Moayyed
* @since 3.3
*/
public interface URLConnectionFactory {
/**
* Receives a {@link URLConnection} instance typically as a result of a {@link URL}
* opening a connection to a remote resource. The received url connection is then
* configured and prepared appropriately depending on its type and is then returned to the caller
* to accommodate method chaining.
*
* @param url The url connection that needs to be configured
* @return The configured {@link URLConnection} instance
*
* @see {@link HttpsURLConnectionFactory}
*/
URLConnection getURLConnection(final URLConnection url);
}

View File

@ -43,10 +43,6 @@ public abstract class AbstractCasProtocolUrlBasedTicketValidator extends Abstrac
* Retrieves the response from the server by opening a connection and merely reading the response.
*/
protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
if (this.hostnameVerifier != null) {
return CommonUtils.getResponseFromServer(validationUrl, this.hostnameVerifier, getEncoding());
} else {
return CommonUtils.getResponseFromServer(validationUrl, getEncoding());
}
return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
}
}

View File

@ -31,6 +31,8 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.FileInputStream;
import java.util.Properties;
/**
* The filter that handles all the work of validating ticket requests.
@ -81,6 +83,31 @@ public abstract class AbstractTicketValidationFilter extends AbstractCasFilter {
return this.ticketValidator;
}
/**
* Gets the ssl config to use for HTTPS connections
* if one is configured for this filter.
* @param filterConfig Servlet filter configuration.
* @return Properties that can contains key/trust info for Client Side Certificates
*/
protected Properties getSSLConfig(final FilterConfig filterConfig) {
final Properties properties = new Properties();
final String fileName = getPropertyFromInitParams(filterConfig, "sslConfigFile", null);
if (fileName != null) {
FileInputStream fis = null;
try {
fis = new FileInputStream(fileName);
properties.load(fis);
logger.trace("Loaded {} entries from {}", properties.size(), fileName);
} catch(final IOException ioe) {
logger.error(ioe.getMessage(), ioe);
} finally {
CommonUtils.closeQuietly(fis);
}
}
return properties;
}
/**
* Gets the configured {@link HostnameVerifier} to use for HTTPS connections
* if one is configured for this filter.

View File

@ -19,6 +19,8 @@
package org.jasig.cas.client.validation;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.URLConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -29,24 +31,22 @@ import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
/**
* Abstract validator implementation for tickets that must be validated against a server.
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.1
*/
public abstract class AbstractUrlBasedTicketValidator implements TicketValidator {
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Hostname verifier used when making an SSL request to the CAS server.
* URLConnection factory instance to use when making validation requests to the CAS server.
* Defaults to {@link HttpsURLConnectionFactory}
*/
protected HostnameVerifier hostnameVerifier;
private URLConnectionFactory urlConnectionFactory = new HttpsURLConnectionFactory();
/**
* Prefix for the CAS server. Should be everything up to the url endpoint, including the /.
*
@ -217,10 +217,6 @@ public abstract class AbstractUrlBasedTicketValidator implements TicketValidator
public final void setCustomParameters(final Map<String,String> customParameters) {
this.customParameters = customParameters;
}
public final void setHostnameVerifier(final HostnameVerifier verifier) {
this.hostnameVerifier = verifier;
}
public final void setEncoding(final String encoding) {
this.encoding = encoding;
@ -241,4 +237,12 @@ public abstract class AbstractUrlBasedTicketValidator implements TicketValidator
protected final Map<String, String> getCustomParameters() {
return this.customParameters;
}
}
protected URLConnectionFactory getURLConnectionFactory() {
return this.urlConnectionFactory;
}
public void setURLConnectionFactory(final URLConnectionFactory urlConnectionFactory) {
this.urlConnectionFactory = urlConnectionFactory;
}
}

View File

@ -20,6 +20,9 @@ package org.jasig.cas.client.validation;
import javax.servlet.FilterConfig;
import org.jasig.cas.client.util.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.URLConnectionFactory;
/**
* Implementation of AbstractTicketValidatorFilter that instanciates a Cas10TicketValidator.
* <p>Deployers can provide the "casServerPrefix" and the "renew" attributes via the standard context or filter init
@ -35,7 +38,9 @@ public class Cas10TicketValidationFilter extends AbstractTicketValidationFilter
final String casServerUrlPrefix = getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null);
final Cas10TicketValidator validator = new Cas10TicketValidator(casServerUrlPrefix);
validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
validator.setHostnameVerifier(getHostnameVerifier(filterConfig));
final URLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(filterConfig), getSSLConfig(filterConfig));
validator.setURLConnectionFactory(factory);
validator.setEncoding(getPropertyFromInitParams(filterConfig, "encoding", null));
return validator;

View File

@ -31,7 +31,9 @@ import javax.servlet.http.HttpServletResponse;
import org.jasig.cas.client.proxy.*;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.ReflectUtils;
import org.jasig.cas.client.util.URLConnectionFactory;
/**
* Creates either a CAS20ProxyTicketValidator or a CAS20ServiceTicketValidator depending on whether any of the
@ -159,7 +161,8 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
}
validator.setCustomParameters(additionalParameters);
validator.setHostnameVerifier(getHostnameVerifier(filterConfig));
final URLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(filterConfig), getSSLConfig(filterConfig));
validator.setURLConnectionFactory(factory);
return validator;
}

View File

@ -21,6 +21,9 @@ package org.jasig.cas.client.validation;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import org.jasig.cas.client.util.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.URLConnectionFactory;
/**
* Implementation of TicketValidationFilter that can instanciate a SAML 1.1 Ticket Validator.
* <p>
@ -53,7 +56,10 @@ public class Saml11TicketValidationFilter extends AbstractTicketValidationFilter
final String tolerance = getPropertyFromInitParams(filterConfig, "tolerance", "1000");
validator.setTolerance(Long.parseLong(tolerance));
validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
validator.setHostnameVerifier(getHostnameVerifier(filterConfig));
final URLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(filterConfig), getSSLConfig(filterConfig));
validator.setURLConnectionFactory(factory);
validator.setEncoding(getPropertyFromInitParams(filterConfig, "encoding", null));
validator.setDisableXmlSchemaValidation(parseBoolean(getPropertyFromInitParams(filterConfig, "disableXmlSchemaValidation", "false")));
return validator;

View File

@ -45,13 +45,10 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.*;
import javax.net.ssl.HttpsURLConnection;
/**
* TicketValidator that can understand validating a SAML artifact. This includes the SOAP request/response.
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.1
*/
public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator {
@ -232,12 +229,11 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
+ "<samlp:AssertionArtifact>" + ticket
+ "</samlp:AssertionArtifact></samlp:Request></SOAP-ENV:Body></SOAP-ENV:Envelope>";
HttpURLConnection conn = null;
DataOutputStream out = null;
BufferedReader in = null;
try {
conn = (HttpURLConnection) validationUrl.openConnection();
if (this.hostnameVerifier != null && conn instanceof HttpsURLConnection) {
((HttpsURLConnection)conn).setHostnameVerifier(this.hostnameVerifier);
}
conn = (HttpURLConnection) this.getURLConnectionFactory().getURLConnection(validationUrl.openConnection());
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "text/xml");
conn.setRequestProperty("Content-Length", Integer.toString(MESSAGE_TO_SEND.length()));
@ -246,12 +242,11 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
conn.setDoInput(true);
conn.setDoOutput(true);
final DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out = new DataOutputStream(conn.getOutputStream());
out.writeBytes(MESSAGE_TO_SEND);
out.flush();
out.close();
final BufferedReader in = new BufferedReader(CommonUtils.isNotBlank(getEncoding()) ? new InputStreamReader(conn.getInputStream(), Charset.forName(getEncoding())) : new InputStreamReader(conn.getInputStream()));
in = new BufferedReader(CommonUtils.isNotBlank(getEncoding()) ? new InputStreamReader(conn.getInputStream(), Charset.forName(getEncoding())) : new InputStreamReader(conn.getInputStream()));
final StringBuilder buffer = new StringBuilder(256);
String line;
@ -263,6 +258,8 @@ public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator
} catch (final IOException e) {
throw new RuntimeException(e);
} finally {
CommonUtils.closeQuietly(out);
CommonUtils.closeQuietly(in);
if (conn != null) {
conn.disconnect();
}

View File

@ -75,6 +75,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
@ -94,6 +95,7 @@
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>