Add support for unique service principal.

Some JAAS frameworks (e.g. JBoss 5) may create an implicit principal from
username/password credentials passed into the JAAS pipeline. Since the CAS
service URL is used for the username part of the credential, support has
been added to append a unique suffix to the service URL and make it
available to the custom AssertionPrincpal princpal type.

A unqiue URL is generated prior to invoking WebAuthentication#login() to
account for this feature of JBoss 5.
This commit is contained in:
Marvin S. Addison 2013-12-20 10:40:35 -05:00
parent bd2c35a38f
commit f9dfd6cf2f
6 changed files with 65 additions and 17 deletions

View File

@ -33,19 +33,24 @@ import org.jasig.cas.client.validation.Assertion;
public class AssertionPrincipal extends SimplePrincipal implements Serializable {
/** AssertionPrincipal.java */
private static final long serialVersionUID = 2288520214366461693L;
private static final long serialVersionUID = 1572526837434920575L;
/** CAS assertion describing authenticated state */
private Assertion assertion;
/** Service principal ID. */
private String servicePrincipal;
/**
* Creates a new principal containing the CAS assertion.
*
* @param name Principal name.
* @param servicePrincipal Service principal ID.
* @param assertion CAS assertion.
*/
public AssertionPrincipal(final String name, final Assertion assertion) {
public AssertionPrincipal(final String name, final String servicePrincipal, final Assertion assertion) {
super(name);
this.servicePrincipal = servicePrincipal;
this.assertion = assertion;
}
@ -55,4 +60,11 @@ public class AssertionPrincipal extends SimplePrincipal implements Serializable
public Assertion getAssertion() {
return this.assertion;
}
/**
* @return Service principal ID.
*/
public String getServicePrincipal() {
return servicePrincipal;
}
}

View File

@ -27,6 +27,9 @@ import java.security.Principal;
import java.security.acl.Group;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
@ -134,6 +137,10 @@ public class CasLoginModule implements LoginModule {
*/
protected static final Map<TicketCredential, Assertion> ASSERTION_CACHE = new HashMap<TicketCredential, Assertion>();
/** Unique suffix pattern appended to some service URLs to make them unique. */
protected static final Pattern UNIQUE_SUFFIX_PATTERN =
Pattern.compile(".*;uuid=\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}");
/** Logger instance */
protected final Logger logger = LoggerFactory.getLogger(getClass());
@ -146,9 +153,12 @@ public class CasLoginModule implements LoginModule {
/** CAS ticket validator */
protected TicketValidator ticketValidator;
/** CAS service parameter used if no service is provided via TextCallback on login */
/** CAS ticket validation URL. */
protected String service;
/** Service principal. */
protected String servicePrincipal;
/** CAS assertion */
protected Assertion assertion;
@ -179,6 +189,16 @@ public class CasLoginModule implements LoginModule {
/** Units of cache timeout. */
protected TimeUnit cacheTimeoutUnit = DEFAULT_CACHE_TIMEOUT_UNIT;
/**
* @return Unique suffix suitable for appending to service URLs to make them unique.
*/
public static String createUniqueSuffix()
{
return ";uuid=" + UUID.randomUUID();
}
/**
* Initializes the CAS login module.
*
@ -302,11 +322,20 @@ public class CasLoginModule implements LoginModule {
if (ticketCallback.getPassword() != null) {
this.ticket = new TicketCredential(new String(ticketCallback.getPassword()));
final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName()
: this.service;
if (CommonUtils.isBlank(this.service)) {
this.service = serviceCallback.getName();
final Matcher m = UNIQUE_SUFFIX_PATTERN.matcher(this.service);
if (m.matches()) {
this.servicePrincipal = this.service;
this.service = m.replaceFirst("");
} else {
this.servicePrincipal = this.service + createUniqueSuffix();
}
} else {
this.servicePrincipal = this.service + createUniqueSuffix();
}
if (this.cacheAssertions) {
this.assertion = ASSERTION_CACHE.get(ticket);
this.assertion = ASSERTION_CACHE.get(this.ticket);
if (this.assertion != null) {
logger.debug("Assertion found in cache.");
}
@ -320,9 +349,11 @@ public class CasLoginModule implements LoginModule {
"Neither login module nor callback handler provided required service parameter.");
}
try {
logger.debug("Attempting ticket validation with service={} and ticket={}", service,
logger.debug(
"Attempting ticket validation with service={} and ticket={}",
this.service,
this.ticket);
this.assertion = this.ticketValidator.validate(this.ticket.getName(), service);
this.assertion = this.ticketValidator.validate(this.ticket.getName(), this.service);
} catch (final Exception e) {
logger.info("Login failed due to CAS ticket validation failure: {}", e);
@ -383,7 +414,9 @@ public class CasLoginModule implements LoginModule {
throw new LoginException("Ticket credential not found.");
}
final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(),
final AssertionPrincipal casPrincipal = new AssertionPrincipal(
this.assertion.getPrincipal().getName(),
this.servicePrincipal,
this.assertion);
this.subject.getPrincipals().add(casPrincipal);
@ -396,7 +429,7 @@ public class CasLoginModule implements LoginModule {
// Add group principal containing role data
final Group roleGroup = new SimpleGroup(this.roleGroupName);
for (final String defaultRole : defaultRoles) {
for (final String defaultRole : this.defaultRoles) {
roleGroup.addMember(new SimplePrincipal(defaultRole));
}
@ -417,9 +450,9 @@ public class CasLoginModule implements LoginModule {
this.subject.getPrincipals().add(roleGroup);
// Place principal name in shared state for downstream JAAS modules (module chaining use case)
this.sharedState.put(LOGIN_NAME, assertion.getPrincipal().getName());
this.sharedState.put(LOGIN_NAME, this.assertion.getPrincipal().getName());
logger.debug("Created JAAS subject with principals: {}", subject.getPrincipals());
logger.debug("Created JAAS subject with principals: {}", this.subject.getPrincipals());
if (this.cacheAssertions) {
logger.debug("Caching assertion for principal {}", this.assertion.getPrincipal());

View File

@ -72,7 +72,7 @@ public class SerializationTests extends TestCase {
final SimpleGroup simpleGroup = new SimpleGroup("group");
final AttributePrincipalImpl attributePrincipal = new AttributePrincipalImpl("attr",
Collections.<String, Object> singletonMap("LOA", "3"));
final AssertionPrincipal assertionPrincipal = new AssertionPrincipal("assertion", new AssertionImpl(
final AssertionPrincipal assertionPrincipal = new AssertionPrincipal("assertion", "foo", new AssertionImpl(
attributePrincipal, Collections.<String, Object> singletonMap("authenticationMethod", "username")));
return new Object[] { simplePrincipal, simpleGroup, attributePrincipal, assertionPrincipal, };

View File

@ -560,7 +560,7 @@
<repository>
<id>atlassian</id>
<name>Atlassian Repository</name>
<url>http://repository.atlassian.com/maven2/</url>
<url>https://maven.atlassian.com/content/groups/public/</url>
</repository>
</repositories>
</project>

View File

@ -20,6 +20,8 @@ package org.jasig.cas.client.jboss.authentication;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.UUID;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
@ -28,6 +30,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.jasig.cas.client.jaas.AssertionPrincipal;
import org.jasig.cas.client.jaas.CasLoginModule;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jboss.web.tomcat.security.login.WebAuthentication;
@ -61,7 +64,7 @@ public final class WebAuthenticationFilter extends AbstractCasFilter {
try {
final String service = constructServiceUrl(request, response);
logger.debug("Attempting CAS ticket validation with service={} and ticket={}", service, ticket);
if (!new WebAuthentication().login(service, ticket)) {
if (!new WebAuthentication().login(service + CasLoginModule.createUniqueSuffix(), ticket)) {
logger.debug("JBoss Web authentication failed.");
throw new GeneralSecurityException("JBoss Web authentication failed.");
}

View File

@ -8,7 +8,7 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.jasig.cas.client</groupId>
<version>3.3-SNAPSHOT</version>
<version>3.3-jaas-unique-principal</version>
<artifactId>cas-client</artifactId>
<packaging>pom</packaging>