diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java
index 58a3e0e..02454e8 100644
--- a/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java
+++ b/cas-client-core/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java
@@ -26,9 +26,15 @@ import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.security.Principal;
import java.security.acl.Group;
-import java.util.*;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
@@ -79,6 +85,8 @@ import org.jasig.cas.client.validation.TicketValidator;
* for JAAS providers that attempt to periodically reauthenticate to renew principal.
* Since CAS tickets are one-time-use, a cached assertion must be provided on reauthentication.
*
cacheTimeout (optional) - Assertion cache timeout in minutes.
+ * cacheTimeoutUnit (optional) - Assertion cache timeout unit. Must be one of {@link TimeUnit} enumeration
+ * names, e.g. DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS. Default unit is MINUTES.
*
*
*
@@ -129,6 +137,9 @@ public class CasLoginModule implements LoginModule {
*/
public static final int DEFAULT_CACHE_TIMEOUT = 480;
+ /** Default assertion cache timeout unit is minutes. */
+ public static final TimeUnit DEFAULT_CACHE_TIMEOUT_UNIT = TimeUnit.MINUTES;
+
/**
* Stores mapping of ticket to assertion to support JAAS providers that
* attempt to periodically re-authenticate to renew principal. Since
@@ -137,9 +148,6 @@ public class CasLoginModule implements LoginModule {
*/
protected static final Map ASSERTION_CACHE = new HashMap();
- /** Executor responsible for assertion cache cleanup */
- protected static Executor cacheCleanerExecutor = Executors.newSingleThreadExecutor();
-
/** Logger instance */
protected final Log log = LogFactory.getLog(getClass());
@@ -182,8 +190,12 @@ public class CasLoginModule implements LoginModule {
/** Assertion cache timeout in minutes */
protected int cacheTimeout = DEFAULT_CACHE_TIMEOUT;
+ /** Units of cache timeout. */
+ protected TimeUnit cacheTimeoutUnit = DEFAULT_CACHE_TIMEOUT_UNIT;
+
/**
* Initializes the CAS login module.
+ *
* @param subject Authentication subject.
* @param handler Callback handler.
* @param state Shared state map.
@@ -201,15 +213,20 @@ public class CasLoginModule implements LoginModule {
* which by default are single use, reauthentication fails. Assertion caching addresses this
* behavior.
* cacheTimeout (optional) - assertion cache timeout in minutes.
+ * cacheTimeoutUnit (optional) - Assertion cache timeout unit. Must be one of {@link TimeUnit} enumeration
+ * names, e.g. DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS. Default unit is MINUTES.
*
*/
+ public final void initialize(
+ final Subject subject,
+ final CallbackHandler handler,
+ final Map state,
+ final Map options) {
-
- public final void initialize(final Subject subject, final CallbackHandler handler, final Map state, final Map options) {
this.assertion = null;
this.callbackHandler = handler;
this.subject = subject;
- this.sharedState = (Map) state;
+ this.sharedState = (Map) state;
this.sharedState = new HashMap(state);
String ticketValidatorClass = null;
@@ -245,11 +262,14 @@ public class CasLoginModule implements LoginModule {
} else if ("cacheTimeout".equals(key)) {
this.cacheTimeout = Integer.parseInt((String) options.get(key));
log.debug("Set cacheTimeout=" + this.cacheTimeout);
+ } else if ("cacheTimeoutUnit".equals(key)) {
+ this.cacheTimeoutUnit = Enum.valueOf(TimeUnit.class, (String) options.get(key));
+ log.debug("Set cacheTimeoutUnit=" + this.cacheTimeoutUnit);
}
}
if (this.cacheAssertions) {
- cacheCleanerExecutor.execute(new CacheCleaner());
+ cleanCache();
}
CommonUtils.assertNotNull(ticketValidatorClass, "ticketValidatorClass is required.");
@@ -293,19 +313,19 @@ public class CasLoginModule implements LoginModule {
throw (LoginException) new LoginException("IO exception in callback handler: " + e).initCause(e);
} catch (final UnsupportedCallbackException e) {
log.info("Login failed due to unsupported callback: " + e);
- throw (LoginException) new LoginException("Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e);
+ throw (LoginException) new LoginException(
+ "Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e);
}
if (ticketCallback.getPassword() != null) {
this.ticket = new TicketCredential(new String(ticketCallback.getPassword()));
- final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service;
+ final String service = CommonUtils.isNotBlank(
+ serviceCallback.getName()) ? serviceCallback.getName() : this.service;
if (this.cacheAssertions) {
- synchronized(ASSERTION_CACHE) {
- if (ASSERTION_CACHE.get(ticket) != null) {
- log.debug("Assertion found in cache.");
- this.assertion = ASSERTION_CACHE.get(ticket);
- }
+ this.assertion = ASSERTION_CACHE.get(ticket);
+ if (this.assertion != null) {
+ log.debug("Assertion found in cache.");
}
}
@@ -313,7 +333,8 @@ public class CasLoginModule implements LoginModule {
log.debug("CAS assertion is null; ticket validation required.");
if (CommonUtils.isBlank(service)) {
log.info("Login failed because required CAS service parameter not provided.");
- throw new LoginException("Neither login module nor callback handler provided required service parameter.");
+ throw new LoginException(
+ "Neither login module nor callback handler provided required service parameter.");
}
try {
if (log.isDebugEnabled()) {
@@ -380,7 +401,8 @@ public class CasLoginModule implements LoginModule {
throw new LoginException("Ticket credential not found.");
}
- final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion);
+ final AssertionPrincipal casPrincipal = new AssertionPrincipal(
+ this.assertion.getPrincipal().getName(), this.assertion);
this.subject.getPrincipals().add(casPrincipal);
// Add group containing principal as sole member
@@ -486,10 +508,12 @@ public class CasLoginModule implements LoginModule {
* @return Ticket validator with properties set.
*/
private TicketValidator createTicketValidator(final String className, final Map propertyMap) {
- CommonUtils.assertTrue(propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found.");
+ CommonUtils.assertTrue(
+ propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found.");
final Class validatorClass = ReflectUtils.loadClass(className);
- final TicketValidator validator = ReflectUtils.newInstance(validatorClass, propertyMap.get("casServerUrlPrefix"));
+ final TicketValidator validator = ReflectUtils.newInstance(
+ validatorClass, propertyMap.get("casServerUrlPrefix"));
try {
final BeanInfo info = Introspector.getBeanInfo(validatorClass);
@@ -534,7 +558,8 @@ public class CasLoginModule implements LoginModule {
} else if (long.class.equals(pd.getPropertyType())) {
return new Long(value);
} else {
- throw new IllegalArgumentException("No conversion strategy exists for property " + pd.getName() + " of type " + pd.getPropertyType());
+ throw new IllegalArgumentException(
+ "No conversion strategy exists for property " + pd.getName() + " of type " + pd.getPropertyType());
}
}
@@ -554,27 +579,28 @@ public class CasLoginModule implements LoginModule {
this.subject.getPrivateCredentials().removeAll(this.subject.getPrivateCredentials(clazz));
}
- /** Removes expired entries from the assertion cache. */
- private class CacheCleaner implements Runnable {
- public void run() {
- if (log.isDebugEnabled()) {
- log.debug("Cleaning assertion cache of size " + CasLoginModule.ASSERTION_CACHE.size());
- }
- final Iterator> iter =
- CasLoginModule.ASSERTION_CACHE.entrySet().iterator();
- final Calendar cutoff = Calendar.getInstance();
- cutoff.add(Calendar.MINUTE, -CasLoginModule.this.cacheTimeout);
- while (iter.hasNext()) {
- final Assertion assertion = iter.next().getValue();
- final Calendar created = Calendar.getInstance();
- created.setTime(assertion.getValidFromDate());
- if (created.before(cutoff)) {
- if (log.isDebugEnabled()) {
- log.debug("Removing expired assertion for principal " + assertion.getPrincipal());
- }
- iter.remove();
+
+ /**
+ * Removes expired entries from the assertion cache.
+ */
+ private void cleanCache() {
+ if (log.isDebugEnabled()) {
+ log.debug("Cleaning assertion cache of size " + ASSERTION_CACHE.size());
+ }
+ final Iterator> iter = ASSERTION_CACHE.entrySet().iterator();
+ final Calendar cutoff = Calendar.getInstance();
+ cutoff.setTimeInMillis(System.currentTimeMillis() - this.cacheTimeoutUnit.toMillis(this.cacheTimeout));
+ while (iter.hasNext()) {
+ final Assertion assertion = iter.next().getValue();
+ final Calendar created = Calendar.getInstance();
+ created.setTime(assertion.getValidFromDate());
+ if (created.before(cutoff)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Removing expired assertion for principal " + assertion.getPrincipal());
}
+ iter.remove();
}
}
}
}
+
diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java
index 623730a..ede32b0 100644
--- a/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java
+++ b/cas-client-core/src/test/java/org/jasig/cas/client/jaas/CasLoginModuleTests.java
@@ -28,13 +28,16 @@ import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
-import static org.junit.Assert.*;
-
import org.jasig.cas.client.PublicTestHttpServer;
-import org.junit.AfterClass;
+import org.jasig.cas.client.validation.TicketValidationException;
import org.junit.Before;
import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
/**
* Unit test for {@link CasLoginModule} class.
*
@@ -150,15 +153,16 @@ public class CasLoginModuleTests {
final String USERNAME = "username";
final String SERVICE = "https://example.com/service";
final String TICKET = "ST-300000-aA5Yuvrxzpv8Tau1cYQ7-srv1";
- final String RESPONSE1 = ""
+ final String SUCCESS_RESPONSE = ""
+ ""
+ USERNAME
+ "";
- final String RESPONSE2 = "Ticket ST-300000-aA5Yuvrxzpv8Tau1cYQ7-srv1 not recognized";
- server.content = RESPONSE1.getBytes(server.encoding);
-
+ final String FAILURE_RESPONSE = "Ticket ST-300000-aA5Yuvrxzpv8Tau1cYQ7-srv1 not recognized";
+
options.put("cacheAssertions", "true");
options.put("cacheTimeout", "1");
+
+ server.content = SUCCESS_RESPONSE.getBytes(server.encoding);
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
@@ -173,7 +177,7 @@ public class CasLoginModuleTests {
module.logout();
assertEquals(0, subject.getPrincipals().size());
assertEquals(0, subject.getPrivateCredentials().size());
- server.content = RESPONSE2.getBytes(server.encoding);
+ server.content = FAILURE_RESPONSE.getBytes(server.encoding);
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
@@ -184,6 +188,54 @@ public class CasLoginModuleTests {
assertEquals(this.subject.getPrincipals().size(), 3);
assertEquals(TICKET, this.subject.getPrivateCredentials().iterator().next().toString());
}
+
+
+ /**
+ * Verify that cached assertions that are expired are never be accessible
+ * by {@link org.jasig.cas.client.jaas.CasLoginModule#login()} method.
+ *
+ * @throws Exception On errors.
+ */
+ @Test
+ public void testAssertionCachingExpiration() throws Exception {
+ final String USERNAME = "hizzy";
+ final String SERVICE = "https://example.com/service";
+ final String TICKET = "ST-12345-ABCDEFGHIJKLMNOPQRSTUVWXYZ-hosta";
+ final String SUCCESS_RESPONSE = ""
+ + ""
+ + USERNAME
+ + "";
+ final String FAILURE_RESPONSE = "Ticket ST-12345-ABCDEFGHIJKLMNOPQRSTUVWXYZ-hosta not recognized";
+
+ options.put("cacheAssertions", "true");
+ // Cache timeout is 1 second
+ options.put("cacheTimeoutUnit", "SECONDS");
+ options.put("cacheTimeout", "1");
+
+ server.content = SUCCESS_RESPONSE.getBytes(server.encoding);
+ module.initialize(
+ subject,
+ new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
+ new HashMap(),
+ options);
+ assertTrue(module.login());
+ module.commit();
+
+ Thread.sleep(1100);
+ // Assertion should now be expired from cache
+ server.content = FAILURE_RESPONSE.getBytes(server.encoding);
+ module.initialize(
+ subject,
+ new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
+ new HashMap(),
+ options);
+ try {
+ module.login();
+ fail("Should have thrown login exception.");
+ } catch (LoginException e) {
+ assertTrue(e.getCause() instanceof TicketValidationException);
+ }
+ }
private boolean hasPrincipalName(final Subject subject, final Class extends Principal> principalClass, final String name) {
final Set extends Principal> principals = subject.getPrincipals(principalClass);
diff --git a/cas-client-core/src/test/resources/log4j.properties b/cas-client-core/src/test/resources/log4j.properties
new file mode 100644
index 0000000..8ccad3b
--- /dev/null
+++ b/cas-client-core/src/test/resources/log4j.properties
@@ -0,0 +1,30 @@
+#
+# log4j configuration to get clean console listing during Maven tests
+#
+
+#
+# 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.
+#
+
+log4j.rootCategory=INFO, stdout
+log4j.logger.org.apache.xml.security=OFF
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%-5p %d{ISO8601} %t::%c{1} - %m%n
+
+log4j.logger.org.jasig.cas=DEBUG