Skip to content

Commit 62f9058

Browse files
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: apereo#26 ...and applies the suggestions and fixes that were brought to light during the code review.
1 parent ed0446f commit 62f9058

12 files changed

Lines changed: 269 additions & 56 deletions

cas-client-core/src/main/java/org/jasig/cas/client/proxy/Cas20ProxyRetriever.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,19 @@
2929
/**
3030
* Implementation of a ProxyRetriever that follows the CAS 2.0 specification.
3131
* For more information on the CAS 2.0 specification, please see the <a
32-
* href="http://www.ja-sig.org/products/cas/overview/protocol/index.html">specification
32+
* href="http://www.jasig.org/cas/protocol">specification
3333
* document</a>.
3434
* <p/>
3535
* In general, this class will make a call to the CAS server with some specified
3636
* parameters and receive an XML response to parse.
3737
*
3838
* @author Scott Battaglia
39-
* @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $
4039
* @since 3.0
4140
*/
4241
public final class Cas20ProxyRetriever implements ProxyRetriever {
4342

4443
/** Unique Id for serialization. */
45-
private static final long serialVersionUID = 560409469568911791L;
44+
private static final long serialVersionUID = 560409469568911791L;
4645

4746
/**
4847
* Instance of Commons Logging.

cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@
2424
import org.slf4j.Logger;
2525
import org.slf4j.LoggerFactory;
2626

27-
import javax.net.ssl.HostnameVerifier;
28-
import javax.net.ssl.HttpsURLConnection;
27+
2928
import javax.servlet.http.HttpServletRequest;
3029
import javax.servlet.http.HttpServletResponse;
3130

31+
import java.io.Closeable;
3232
import java.io.IOException;
3333
import java.io.UnsupportedEncodingException;
3434
import java.io.BufferedReader;
3535
import java.io.InputStreamReader;
36+
import java.net.MalformedURLException;
3637
import java.net.URLConnection;
3738
import java.net.URLEncoder;
3839
import java.net.URL;
3940
import java.net.HttpURLConnection;
40-
import java.net.MalformedURLException;
4141
import java.text.DateFormat;
4242
import java.text.SimpleDateFormat;
4343
import java.util.*;
@@ -323,28 +323,16 @@ public static String safeGetParameter(final HttpServletRequest request, final St
323323
* Contacts the remote URL and returns the response.
324324
*
325325
* @param constructedUrl the url to contact.
326+
* @param factory connection factory to prepare the URL connection instance
326327
* @param encoding the encoding to use.
327328
* @return the response.
328329
*/
329-
public static String getResponseFromServer(final URL constructedUrl, final String encoding) {
330-
return getResponseFromServer(constructedUrl, HttpsURLConnection.getDefaultHostnameVerifier(), encoding);
331-
}
330+
public static String getResponseFromServer(final URL constructedUrl, final URLConnectionFactory factory, final String encoding) {
332331

333-
/**
334-
* Contacts the remote URL and returns the response.
335-
*
336-
* @param constructedUrl the url to contact.
337-
* @param hostnameVerifier Host name verifier to use for HTTPS connections.
338-
* @param encoding the encoding to use.
339-
* @return the response.
340-
*/
341-
public static String getResponseFromServer(final URL constructedUrl, final HostnameVerifier hostnameVerifier, final String encoding) {
342332
URLConnection conn = null;
343333
try {
344-
conn = constructedUrl.openConnection();
345-
if (conn instanceof HttpsURLConnection) {
346-
((HttpsURLConnection)conn).setHostnameVerifier(hostnameVerifier);
347-
}
334+
conn = factory.getURLConnection(constructedUrl.openConnection());
335+
348336
final BufferedReader in;
349337

350338
if (CommonUtils.isEmpty(encoding)) {
@@ -371,6 +359,7 @@ public static String getResponseFromServer(final URL constructedUrl, final Hostn
371359
}
372360

373361
}
362+
374363
/**
375364
* Contacts the remote URL and returns the response.
376365
*
@@ -380,12 +369,12 @@ public static String getResponseFromServer(final URL constructedUrl, final Hostn
380369
*/
381370
public static String getResponseFromServer(final String url, String encoding) {
382371
try {
383-
return getResponseFromServer(new URL(url), encoding);
372+
return getResponseFromServer(new URL(url), new HttpsURLConnectionFactory(), encoding);
384373
} catch (final MalformedURLException e) {
385374
throw new IllegalArgumentException(e);
386375
}
387376
}
388-
377+
389378
public static ProxyList createProxyList(final String proxies) {
390379
if (CommonUtils.isBlank(proxies)) {
391380
return new ProxyList();
@@ -410,4 +399,19 @@ public static void sendRedirect(final HttpServletResponse response, final String
410399
}
411400

412401
}
402+
403+
/**
404+
* Unconditionally close a {@link Closeable}. Equivalent to {@link java.io.Closeable#close()}close(), except any exceptions
405+
* will be ignored. This is typically used in finally blocks.
406+
* @param resource
407+
*/
408+
public static void closeQuietly(final Closeable resource) {
409+
try {
410+
if (resource != null) {
411+
resource.close();
412+
}
413+
} catch (final IOException e) {
414+
//ignore
415+
}
416+
}
413417
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.jasig.cas.client.util;
2+
3+
import java.io.FileInputStream;
4+
import java.io.InputStream;
5+
import java.net.URLConnection;
6+
import java.security.KeyStore;
7+
import java.util.Properties;
8+
9+
import javax.net.ssl.HostnameVerifier;
10+
import javax.net.ssl.HttpsURLConnection;
11+
import javax.net.ssl.KeyManagerFactory;
12+
import javax.net.ssl.SSLContext;
13+
import javax.net.ssl.SSLSocketFactory;
14+
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
/**
19+
* An implementation of the {@link URLConnectionFactory} whose responsible to configure
20+
* the underlying <i>https</i> connection, if needed, with a given hostname and SSL socket factory based on the
21+
* configuration provided.
22+
*
23+
* @author Misagh Moayyed
24+
* @since 3.3
25+
* @see #setHostnameVerifier(HostnameVerifier)
26+
* @see #setSSLConfiguration(Properties)
27+
*/
28+
public final class HttpsURLConnectionFactory implements URLConnectionFactory {
29+
30+
private static final Logger LOGGER = LoggerFactory.getLogger(HttpsURLConnectionFactory.class);
31+
32+
/**
33+
* Hostname verifier used when making an SSL request to the CAS server.
34+
* Defaults to {@link HttpsURLConnection#getDefaultHostnameVerifier()}
35+
*/
36+
private HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
37+
38+
/**
39+
* Properties file that can contains key/trust info for Client Side Certificates
40+
*/
41+
private Properties sslConfiguration = new Properties();
42+
43+
public HttpsURLConnectionFactory() {}
44+
45+
public HttpsURLConnectionFactory(final HostnameVerifier verifier, final Properties config) {
46+
setHostnameVerifier(verifier);
47+
setSSLConfiguration(config);
48+
}
49+
50+
public final void setSSLConfiguration(final Properties config) {
51+
this.sslConfiguration = config;
52+
}
53+
54+
public final void setHostnameVerifier(final HostnameVerifier verifier) {
55+
this.hostnameVerifier = verifier;
56+
}
57+
58+
public URLConnection getURLConnection(final URLConnection url) {
59+
return this.configureHttpsConnectionIfNeeded(url);
60+
}
61+
62+
/**
63+
* Configures the connection with specific settings for secure http connections
64+
* If the connection instance is not a {@link HttpsURLConnection},
65+
* no additional changes will be made and the connection itself is simply returned.
66+
*
67+
* @param conn the http connection
68+
*/
69+
private URLConnection configureHttpsConnectionIfNeeded(final URLConnection conn) {
70+
if (conn instanceof HttpsURLConnection) {
71+
final HttpsURLConnection httpsConnection = (HttpsURLConnection) conn;
72+
final SSLSocketFactory socketFactory = this.createSSLSocketFactory();
73+
if (socketFactory != null) {
74+
httpsConnection.setSSLSocketFactory(socketFactory);
75+
}
76+
77+
if (this.hostnameVerifier != null) {
78+
httpsConnection.setHostnameVerifier(this.hostnameVerifier);
79+
}
80+
}
81+
return conn;
82+
}
83+
84+
/**
85+
* Creates a {@link SSLSocketFactory} based on the configuration specified
86+
* <p>
87+
* Sample properties file:
88+
* <pre>
89+
* protocol=TLS
90+
* keyStoreType=JKS
91+
* keyStorePath=/var/secure/location/.keystore
92+
* keyStorePass=changeit
93+
* certificatePassword=aGoodPass
94+
* </pre>
95+
* @param sslConfig {@link Properties}
96+
* @return the {@link SSLSocketFactory}
97+
*/
98+
private SSLSocketFactory createSSLSocketFactory() {
99+
InputStream keyStoreIS = null;
100+
try {
101+
final SSLContext sslContext = SSLContext.getInstance(this.sslConfiguration.getProperty("protocol", "SSL"));
102+
103+
if (this.sslConfiguration.getProperty("keyStoreType") != null) {
104+
final KeyStore keyStore = KeyStore.getInstance(this.sslConfiguration.getProperty("keyStoreType"));
105+
if (this.sslConfiguration.getProperty("keyStorePath") != null) {
106+
keyStoreIS = new FileInputStream(this.sslConfiguration.getProperty("keyStorePath"));
107+
if (this.sslConfiguration.getProperty("keyStorePass") != null) {
108+
keyStore.load(keyStoreIS, this.sslConfiguration.getProperty("keyStorePass").toCharArray());
109+
LOGGER.debug("Keystore has {} keys", keyStore.size());
110+
final KeyManagerFactory keyManager = KeyManagerFactory.getInstance(this.sslConfiguration.getProperty("keyManagerType", "SunX509"));
111+
keyManager.init(keyStore, this.sslConfiguration.getProperty("certificatePassword").toCharArray());
112+
sslContext.init(keyManager.getKeyManagers(), null, null);
113+
}
114+
}
115+
}
116+
117+
return sslContext.getSocketFactory();
118+
} catch (final Exception e) {
119+
LOGGER.error(e.getMessage(), e);
120+
} finally {
121+
CommonUtils.closeQuietly(keyStoreIS);
122+
}
123+
return null;
124+
}
125+
126+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Licensed to Jasig under one or more contributor license
3+
* agreements. See the NOTICE file distributed with this work
4+
* for additional information regarding copyright ownership.
5+
* Jasig licenses this file to you under the Apache License,
6+
* Version 2.0 (the "License"); you may not use this file
7+
* except in compliance with the License. You may obtain a
8+
* copy of the License at the following location:
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.jasig.cas.client.util;
20+
21+
import java.net.URL;
22+
import java.net.URLConnection;
23+
24+
/**
25+
* A factory to prepare and configure {@link java.net.URLConnection} instances.
26+
*
27+
* @author Misagh Moayyed
28+
* @since 3.3
29+
*/
30+
public interface URLConnectionFactory {
31+
32+
/**
33+
* Receives a {@link URLConnection} instance typically as a result of a {@link URL}
34+
* opening a connection to a remote resource. The received url connection is then
35+
* configured and prepared appropriately depending on its type and is then returned to the caller
36+
* to accommodate method chaining.
37+
*
38+
* @param url The url connection that needs to be configured
39+
* @return The configured {@link URLConnection} instance
40+
*
41+
* @see {@link HttpsURLConnectionFactory}
42+
*/
43+
URLConnection getURLConnection(final URLConnection url);
44+
}

cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ protected final void setDisableXmlSchemaValidation(final boolean disable) {
4343
* Retrieves the response from the server by opening a connection and merely reading the response.
4444
*/
4545
protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
46-
if (this.hostnameVerifier != null) {
47-
return CommonUtils.getResponseFromServer(validationUrl, this.hostnameVerifier, getEncoding());
48-
} else {
49-
return CommonUtils.getResponseFromServer(validationUrl, getEncoding());
50-
}
46+
return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
5147
}
5248
}

cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import javax.servlet.http.HttpServletRequest;
3232
import javax.servlet.http.HttpServletResponse;
3333
import java.io.IOException;
34+
import java.io.FileInputStream;
35+
import java.util.Properties;
3436

3537
/**
3638
* The filter that handles all the work of validating ticket requests.
@@ -81,6 +83,31 @@ protected TicketValidator getTicketValidator(final FilterConfig filterConfig) {
8183
return this.ticketValidator;
8284
}
8385

86+
/**
87+
* Gets the ssl config to use for HTTPS connections
88+
* if one is configured for this filter.
89+
* @param filterConfig Servlet filter configuration.
90+
* @return Properties that can contains key/trust info for Client Side Certificates
91+
*/
92+
protected Properties getSSLConfig(final FilterConfig filterConfig) {
93+
final Properties properties = new Properties();
94+
final String fileName = getPropertyFromInitParams(filterConfig, "sslConfigFile", null);
95+
96+
if (fileName != null) {
97+
FileInputStream fis = null;
98+
try {
99+
fis = new FileInputStream(fileName);
100+
properties.load(fis);
101+
logger.trace("Loaded {} entries from {}", properties.size(), fileName);
102+
} catch(final IOException ioe) {
103+
logger.error(ioe.getMessage(), ioe);
104+
} finally {
105+
CommonUtils.closeQuietly(fis);
106+
}
107+
}
108+
return properties;
109+
}
110+
84111
/**
85112
* Gets the configured {@link HostnameVerifier} to use for HTTPS connections
86113
* if one is configured for this filter.

0 commit comments

Comments
 (0)