applied Marvin's patch.  Made changes to check for debug enabled before doing any concatenation and to renable the initCause
This commit is contained in:
Scott Battaglia 2010-08-16 03:15:45 +00:00
parent 2449a7a61b
commit 74a8cff651
4 changed files with 314 additions and 53 deletions

View File

@ -23,6 +23,7 @@ public class AssertionPrincipal extends SimplePrincipal implements Serializable
/** AssertionPrincipal.java */
private static final long serialVersionUID = 2288520214366461693L;
/** CAS assertion describing authenticated state */
private Assertion assertion;
/**

View File

@ -12,6 +12,8 @@ import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.security.acl.Group;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
@ -58,6 +60,10 @@ import org.jasig.cas.client.validation.TicketValidator;
* which is suitable for JBoss.</li>
* <li>roleGroupName (optional) - The name of a group principal containing all role data.
* The default value is "Roles", which is suitable for JBoss.</li>
* <li>cacheAssertions (optional) - Flag to enable assertion caching. This may be needed
* 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.</li>
* <li>cacheTimeout (optional) - Assertion cache timeout in minutes.</li>
* </ul>
*
* <p>
@ -81,7 +87,7 @@ import org.jasig.cas.client.validation.TicketValidator;
* </pre>
*
* @author Marvin S. Addison
* @version $Revision$
* @version $Revision$ $Date$
* @since 3.1.11
*
*/
@ -102,6 +108,22 @@ public class CasLoginModule implements LoginModule {
* support other JEE containers.
*/
public static final String DEFAULT_ROLE_GROUP_NAME = "Roles";
/**
* Default assertion cache timeout in minutes. Default is 8 hours.
*/
public static final int DEFAULT_CACHE_TIMEOUT = 480;
/**
* Stores mapping of ticket to assertion to support 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.
*/
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());
@ -121,6 +143,9 @@ public class CasLoginModule implements LoginModule {
/** CAS assertion */
protected Assertion assertion;
/** CAS ticket credential */
protected TicketCredential ticket;
/** Login module shared state */
protected Map sharedState;
@ -136,6 +161,12 @@ public class CasLoginModule implements LoginModule {
/** Name of JAAS Group containing role data */
protected String roleGroupName = DEFAULT_ROLE_GROUP_NAME;
/** Enables or disable assertion caching */
protected boolean cacheAssertions;
/** Assertion cache timeout in minutes */
protected int cacheTimeout;
/**
* Initializes the CAS login module.
@ -144,15 +175,22 @@ public class CasLoginModule implements LoginModule {
* @param state Shared state map.
* @param options Login module options. The following are supported:
* <ul>
* <li>service - CAS service URL used for service ticket validation</li>
* <li>ticketValidatorClass - fully-qualified class name of service ticket validator component</li>
* <li>defaultRoles (optional) - comma-delimited list of roles to be added to all authenticated principals</li>
* <li>roleAttributeNames (optional) - comma-delimited list of attributes in the CAS assertion that contain role data</li>
* <li>principalGroupName (optional) - name of JAAS Group containing caller principal</li>
* <li>service - CAS service URL used for service ticket validation.</li>
* <li>ticketValidatorClass - fully-qualified class name of service ticket validator component.</li>
* <li>defaultRoles (optional) - comma-delimited list of roles to be added to all authenticated principals.</li>
* <li>roleAttributeNames (optional) - comma-delimited list of attributes in the CAS assertion that contain role data.</li>
* <li>principalGroupName (optional) - name of JAAS Group containing caller principal.</li>
* <li>roleGroupName (optional) - name of JAAS Group containing role data</li>
* <li>cacheAssertions (optional) - whether or not to cache assertions.
* Some JAAS providers attempt to reauthenticate users after an indeterminate
* period of time. Since the credential used for authentication is a CAS ticket,
* which by default are single use, reauthentication fails. Assertion caching addresses this
* behavior.</li>
* <li>cacheTimeout (optional) - assertion cache timeout in minutes.</li>
* </ul>
*/
public 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 = state;
@ -185,8 +223,17 @@ public class CasLoginModule implements LoginModule {
} else if ("roleGroupName".equals(key)) {
this.roleGroupName = (String) options.get(key);
log.debug("Set roleGroupName=" + this.roleGroupName);
} else if ("cacheAssertions".equals(key)) {
this.cacheAssertions = Boolean.parseBoolean((String) options.get(key));
log.debug("Set cacheAssertions=" + this.cacheAssertions);
} else if ("cacheTimeout".equals(key)) {
this.cacheTimeout = Integer.parseInt((String) options.get(key));
log.debug("Set cacheTimeout=" + this.cacheTimeout);
}
}
if (this.cacheAssertions) {
cacheCleanerExecutor.execute(new CacheCleaner());
}
CommonUtils.assertNotNull(ticketValidatorClass, "ticketValidatorClass is required.");
this.ticketValidator = createTicketValidator(ticketValidatorClass, options);
@ -198,41 +245,70 @@ public class CasLoginModule implements LoginModule {
final PasswordCallback ticketCallback = new PasswordCallback("ticket", false);
try {
this.callbackHandler.handle(new Callback[] { ticketCallback, serviceCallback });
} catch (final Exception e) {
throw (LoginException) new LoginException(e.getMessage()).initCause(e);
} catch (final IOException e) {
log.info("Login failed due to IO exception in callback handler: " + e);
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);
}
if (ticketCallback.getPassword() != null) {
final String ticket = new String(ticketCallback.getPassword());
this.ticket = new TicketCredential(new String(ticketCallback.getPassword()));
final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service;
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.");
if (this.cacheAssertions) {
synchronized(ASSERTION_CACHE) {
if (ASSERTION_CACHE.get(ticket) != null) {
log.debug("Assertion found in cache.");
this.assertion = (Assertion) ASSERTION_CACHE.get(ticket);
}
}
}
try {
log.debug("Attempting ticket validation with service=" + service + " and ticket=" + ticket);
this.assertion = this.ticketValidator.validate(ticket, service);
} catch (final Exception e) {
throw (LoginException) new LoginException(e.getMessage()).initCause(e);
}
if (this.assertion == null) {
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.");
}
try {
if (log.isDebugEnabled()) {
log.debug("Attempting ticket validation with service=" + service + " and ticket=" + ticket);
}
this.assertion = this.ticketValidator.validate(this.ticket.getTicket(), service);
} catch (final Exception e) {
log.info("Login failed due to CAS ticket validation failure: " + e);
throw (LoginException) new LoginException("CAS ticket validation failed: " + e).initCause(e);
}
}
log.info("Login succeeded.");
} else {
log.info("Login failed because callback handler did not provide CAS ticket.");
throw new LoginException("Callback handler did not provide CAS ticket.");
}
log.info("Login succeeded.");
return true;
}
public boolean abort() throws LoginException {
if (this.ticket != null) {
this.ticket = null;
}
if (this.assertion != null) {
this.assertion = null;
return true;
}
return false;
return true;
}
public boolean commit() throws LoginException {
if (this.assertion != null) {
if (this.ticket != null) {
this.subject.getPrivateCredentials().add(this.ticket);
} else {
throw new LoginException("Ticket credential not found.");
}
final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion);
this.subject.getPrincipals().add(casPrincipal);
@ -270,29 +346,40 @@ public class CasLoginModule implements LoginModule {
this.sharedState.put(LOGIN_NAME, casPrincipal.getName());
if (log.isDebugEnabled()) {
log.debug("Created JAAS subject with principals: " + subject.getPrincipals());
if (log.isDebugEnabled()) {
log.debug("Created JAAS subject with principals: " + subject.getPrincipals());
}
}
if (this.cacheAssertions) {
if (log.isDebugEnabled()) {
log.debug("Caching assertion for principal " + this.assertion.getPrincipal());
}
ASSERTION_CACHE.put(this.ticket, this.assertion);
}
} else {
// Login must have failed if there is no assertion defined
// Need to clean up state
if (this.ticket != null) {
this.ticket = null;
}
return true;
}
return false;
return true;
}
public boolean logout() throws LoginException {
if (this.assertion != null) {
log.debug("Performing logout.");
this.subject.getPrincipals().remove(this.assertion.getPrincipal());
// Remove all SimpleGroup principals
final Iterator iter = this.subject.getPrincipals().iterator();
while (iter.hasNext()) {
if (iter.next() instanceof SimpleGroup) {
iter.remove();
}
}
this.assertion = null;
log.info("Logout succeeded.");
return true;
}
return false;
log.debug("Performing logout.");
// Remove all CAS principals
removePrincipalsOfType(AssertionPrincipal.class);
removePrincipalsOfType(SimplePrincipal.class);
removePrincipalsOfType(SimpleGroup.class);
// Remove all CAS credentials
removeCredentialsOfType(TicketCredential.class);
log.info("Logout succeeded.");
return true;
}
@ -352,9 +439,52 @@ 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());
}
}
/**
* Removes all principals of the given type from the JAAS subject.
* @param clazz Type of principal to remove.
*/
private void removePrincipalsOfType(final Class clazz) {
final Iterator iter = this.subject.getPrincipals(clazz).iterator();
while (iter.hasNext()) {
this.subject.getPrincipals().remove(iter.next());
}
}
/**
* Removes all credentials of the given type from the JAAS subject.
* @param clazz Type of principal to remove.
*/
private void removeCredentialsOfType(final Class clazz) {
final Iterator iter = this.subject.getPrivateCredentials(clazz).iterator();
while (iter.hasNext()) {
this.subject.getPrivateCredentials().remove(iter.next());
}
}
/** 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 = (Assertion) ((Map.Entry) 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();
}
}
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2010 The JA-SIG Collaborative. All rights reserved. See license
* distributed with this file and available online at
* http://www.ja-sig.org/products/cas/overview/license/index.html
*/
package org.jasig.cas.client.jaas;
/**
* Strongly-typed wrapper for a ticket credential.
*
* @author Marvin S. Addison
* @version $Revision$ $Date$
* @since 3.1.12
*
*/
public final class TicketCredential {
/** Hash code seed value */
private static final int HASHCODE_SEED = 17;
/** Ticket ID string */
private String ticket;
/**
* Creates a new instance that wraps the given ticket.
* @param ticket Ticket identifier string.
*/
public TicketCredential(final String ticket) {
this.ticket = ticket;
}
/**
* @return Ticket identifier string.
*/
public String getTicket() {
return this.ticket;
}
public String toString() {
return this.ticket;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final TicketCredential that = (TicketCredential) o;
if (ticket != null ? !ticket.equals(that.ticket) : that.ticket != null) return false;
return true;
}
public int hashCode() {
int hash = HASHCODE_SEED;
hash = hash * 31 + (ticket == null ? 0 : ticket.hashCode());
return hash;
}
}

View File

@ -15,10 +15,10 @@ import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import org.jasig.cas.client.PublicTestHttpServer;
import junit.framework.TestCase;
import org.jasig.cas.client.PublicTestHttpServer;
/**
* Unit test for {@link CasLoginModule} class.
*
@ -32,6 +32,8 @@ public class CasLoginModuleTests extends TestCase {
private CasLoginModule module;
private Subject subject;
private Map options;
/** {@inheritDoc} */
protected void setUp() throws Exception {
@ -39,7 +41,7 @@ public class CasLoginModuleTests extends TestCase {
module = new CasLoginModule();
subject = new Subject();
final Map options = new HashMap();
options = new HashMap();
options.put("service", "https://service.example.com/webapp");
options.put("ticketValidatorClass", "org.jasig.cas.client.validation.Cas20ServiceTicketValidator");
options.put("casServerUrlPrefix", CONST_CAS_SERVER_URL);
@ -48,27 +50,31 @@ public class CasLoginModuleTests extends TestCase {
options.put("defaultRoles", "ADMIN");
options.put("principalGroupName", "CallerPrincipal");
options.put("roleGroupName", "Roles");
module.initialize(
subject,
new ServiceAndTicketCallbackHandler("myService", "myTicket"),
new HashMap(),
options);
}
/**
* Test JAAS login success.
* @throws Exception On errors.
*/
public void testLoginSuccess() throws Exception {
final String USERNAME = "username";
final String SERVICE = "https://example.com/service";
final String TICKET = "ST-100000-aA5Yuvrxzpv8Tau1cYQ7-srv1";
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
+ "<cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user></cas:authenticationSuccess></cas:serviceResponse>";
PublicTestHttpServer.instance().content = RESPONSE
.getBytes(PublicTestHttpServer.instance().encoding);
PublicTestHttpServer.instance().content = RESPONSE.getBytes(PublicTestHttpServer.instance().encoding);
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
new HashMap(),
options);
module.login();
module.commit();
assertEquals(this.subject.getPrincipals().size(), 3);
assertEquals(TICKET, this.subject.getPrivateCredentials().iterator().next().toString());
assertTrue(hasPrincipalName(this.subject, AssertionPrincipal.class, USERNAME));
assertTrue(hasPrincipalName(this.subject, Group.class, "CallerPrincipal"));
assertTrue(hasPrincipalName(this.subject, Group.class, "Roles"));
@ -76,16 +82,81 @@ public class CasLoginModuleTests extends TestCase {
/**
* Test JAAS login failure.
* @throws Exception On errors.
*/
public void testLoginFailure() throws Exception {
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationFailure code=\"INVALID_TICKET\">Ticket ST-1856339-aA5Yuvrxzpv8Tau1cYQ7 not recognized</cas:authenticationFailure></cas:serviceResponse>";
final String SERVICE = "https://example.com/service";
final String TICKET = "ST-200000-aA5Yuvrxzpv8Tau1cYQ7-srv1";
final String RESPONSE = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationFailure code=\"INVALID_TICKET\">Ticket ST-200000-aA5Yuvrxzpv8Tau1cYQ7-srv1 not recognized</cas:authenticationFailure></cas:serviceResponse>";
PublicTestHttpServer.instance().content = RESPONSE.getBytes(PublicTestHttpServer.instance().encoding);
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
new HashMap(),
options);
try {
module.login();
fail("Login did not throw LoginException as expected.");
} catch (Exception e) {
assertTrue(e instanceof LoginException);
}
module.commit();
assertNull(module.ticket);
assertNull(module.assertion);
}
/**
* Test JAAS logout after successful login to ensure subject cleanup.
* @throws Exception On errors.
*/
public void testLogout() throws Exception {
testLoginSuccess();
module.logout();
assertEquals(0, subject.getPrincipals().size());
assertEquals(0, subject.getPrivateCredentials().size());
}
/**
* Test assertion cache allows successive logins with same ticket to succeed.
* @throws Exception On errors.
*/
public void testAssertionCaching() throws Exception {
final String USERNAME = "username";
final String SERVICE = "https://example.com/service";
final String TICKET = "ST-300000-aA5Yuvrxzpv8Tau1cYQ7-srv1";
final String RESPONSE1 = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
+ "<cas:authenticationSuccess><cas:user>"
+ USERNAME
+ "</cas:user></cas:authenticationSuccess></cas:serviceResponse>";
final String RESPONSE2 = "<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'><cas:authenticationFailure code=\"INVALID_TICKET\">Ticket ST-300000-aA5Yuvrxzpv8Tau1cYQ7-srv1 not recognized</cas:authenticationFailure></cas:serviceResponse>";
PublicTestHttpServer.instance().content = RESPONSE1.getBytes(PublicTestHttpServer.instance().encoding);
options.put("cacheAssertions", "true");
options.put("cacheTimeout", "1");
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
new HashMap(),
options);
module.login();
module.commit();
assertEquals(this.subject.getPrincipals().size(), 3);
assertEquals(TICKET, this.subject.getPrivateCredentials().iterator().next().toString());
Thread.sleep(2000);
module.logout();
assertEquals(0, subject.getPrincipals().size());
assertEquals(0, subject.getPrivateCredentials().size());
PublicTestHttpServer.instance().content = RESPONSE2.getBytes(PublicTestHttpServer.instance().encoding);
module.initialize(
subject,
new ServiceAndTicketCallbackHandler(SERVICE, TICKET),
new HashMap(),
options);
module.login();
module.commit();
assertEquals(this.subject.getPrincipals().size(), 3);
assertEquals(TICKET, this.subject.getPrivateCredentials().iterator().next().toString());
}
private boolean hasPrincipalName(final Subject subject, final Class principalClass, final String name) {