started adding support for supporting creating UserDetails via Assertions.
This commit is contained in:
Scott Battaglia 2009-09-01 19:53:19 +00:00
parent bfd421016e
commit 53baac2fd9
10 changed files with 258 additions and 63 deletions

View File

@ -0,0 +1,20 @@
package org.springframework.security.cas;
/**
* Sets the appropriate parameters for CAS's implementation of SAML (which is not guaranteed to be actually SAML compliant).
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.0
*/
public final class SamlServiceProperties extends ServiceProperties {
public static final String DEFAULT_SAML_ARTIFACT_PARAMETER = "SAMLart";
public static final String DEFAULT_SAML_SERVICE_PARAMETER = "TARGET";
public SamlServiceProperties() {
super.setArtifactParameter(DEFAULT_SAML_ARTIFACT_PARAMETER);
super.setServiceParameter(DEFAULT_SAML_SERVICE_PARAMETER);
}
}

View File

@ -30,15 +30,27 @@ import org.springframework.util.Assert;
* @version $Id$
*/
public class ServiceProperties implements InitializingBean {
public static final String DEFAULT_CAS_ARTIFACT_PARAMETER = "ticket";
public static final String DEFAULT_CAS_SERVICE_PARAMETER = "service";
//~ Instance fields ================================================================================================
private String service;
private boolean sendRenew = false;
private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER;
private String serviceParameter = DEFAULT_CAS_SERVICE_PARAMETER;
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(this.service, "service must be specified.");
Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty.");
Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty.");
}
/**
@ -52,8 +64,8 @@ public class ServiceProperties implements InitializingBean {
*
* @return the URL of the service the user is authenticating to
*/
public String getService() {
return service;
public final String getService() {
return this.service;
}
/**
@ -67,15 +79,41 @@ public class ServiceProperties implements InitializingBean {
*
* @return whether to send the <code>renew</code> parameter to CAS
*/
public boolean isSendRenew() {
return sendRenew;
public final boolean isSendRenew() {
return this.sendRenew;
}
public void setSendRenew(boolean sendRenew) {
public final void setSendRenew(final boolean sendRenew) {
this.sendRenew = sendRenew;
}
public void setService(String service) {
public final void setService(final String service) {
this.service = service;
}
public final String getArtifactParameter() {
return this.artifactParameter;
}
/**
* Configures the Request Parameter to look for when attempting to see if a CAS ticket was sent from the server.
*
* @param artifactParameter the id to use. Default is "ticket".
*/
public final void setArtifactParameter(final String artifactParameter) {
this.artifactParameter = artifactParameter;
}
/**
* Configures the Request parameter to look for when attempting to send a request to CAS.
*
* @return the service parameter to use. Default is "service".
*/
public final String getServiceParameter() {
return serviceParameter;
}
public final void setServiceParameter(final String serviceParameter) {
this.serviceParameter = serviceParameter;
}
}

View File

@ -0,0 +1,41 @@
package org.springframework.security.cas.authentication;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.jasig.cas.client.validation.Assertion;
import java.util.List;
import java.util.ArrayList;
/**
* Temporary authentication object needed to load the user details service.
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.0
*/
public final class CasAssertionAuthenticationToken extends AbstractAuthenticationToken {
private final Assertion assertion;
private final String ticket;
public CasAssertionAuthenticationToken(final Assertion assertion, final String ticket) {
super(new ArrayList<GrantedAuthority>());
this.assertion = assertion;
this.ticket = ticket;
}
public Object getPrincipal() {
return this.assertion.getPrincipal().getName();
}
public Object getCredentials() {
return this.ticket;
}
public Assertion getAssertion() {
return this.assertion;
}
}

View File

@ -31,9 +31,7 @@ import org.springframework.security.cas.web.CasProcessingFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.*;
import org.springframework.util.Assert;
@ -47,13 +45,15 @@ import org.springframework.util.Assert;
* It can also validate a previously created {@link CasAuthenticationToken}.
*
* @author Ben Alex
* @author Scott Battaglia
* @version $Id$
*/
public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
//~ Instance fields ================================================================================================
private UserDetailsService userDetailsService;
private AuthenticationUserDetailsService authenticationUserDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private StatelessTicketCache statelessTicketCache = new NullStatelessTicketCache();
@ -64,7 +64,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A userDetailsService must be set");
Assert.notNull(this.authenticationUserDetailsService, "An authenticationUserDetailsService must be set");
Assert.notNull(this.ticketValidator, "A ticketValidator must be set");
Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
Assert.hasText(this.key, "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
@ -127,7 +127,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
return result;
}
private final CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
try {
final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), serviceProperties.getService());
final UserDetails userDetails = loadUserByAssertion(assertion);
@ -143,18 +143,23 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
* can override this method and retrieve the user based on any criteria they desire.
*
* @param assertion The CAS Assertion.
* @returns the UserDetails.
* @return the UserDetails.
*/
protected UserDetails loadUserByAssertion(final Assertion assertion) {
return this.userDetailsService.loadUserByUsername(assertion.getPrincipal().getName());
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
final CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, "");
return this.authenticationUserDetailsService.loadUserDetails(token);
}
@Deprecated
/**
* @deprecated as of 3.0. Use the {@link org.springframework.security.cas.authentication.CasAuthenticationProvider#setAuthenticationUserDetailsService(org.springframework.security.core.userdetails.AuthenticationUserDetailsService)} instead.
*/
public void setUserDetailsService(final UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
this.authenticationUserDetailsService = new UserDetailsByNameServiceWrapper(userDetailsService);
}
public void setAuthenticationUserDetailsService(final AuthenticationUserDetailsService authenticationUserDetailsService) {
this.authenticationUserDetailsService = authenticationUserDetailsService;
}
public void setServiceProperties(final ServiceProperties serviceProperties) {

View File

@ -51,32 +51,27 @@ public class EhCacheBasedTicketCache implements StatelessTicketCache, Initializi
Assert.notNull(cache, "cache mandatory");
}
public CasAuthenticationToken getByTicketId(String serviceTicket) {
Element element = null;
public CasAuthenticationToken getByTicketId(final String serviceTicket) {
try {
element = cache.get(serviceTicket);
final Element element = cache.get(serviceTicket);
if (logger.isDebugEnabled()) {
logger.debug("Cache hit: " + (element != null) + "; service ticket: " + serviceTicket);
}
return element == null ? null : (CasAuthenticationToken) element.getValue();
} catch (CacheException cacheException) {
throw new DataRetrievalFailureException("Cache failure: " + cacheException.getMessage());
}
if (logger.isDebugEnabled()) {
logger.debug("Cache hit: " + (element != null) + "; service ticket: " + serviceTicket);
}
if (element == null) {
return null;
} else {
return (CasAuthenticationToken) element.getValue();
}
}
public Ehcache getCache() {
return cache;
}
public void putTicketInCache(CasAuthenticationToken token) {
Element element = new Element(token.getCredentials().toString(), token);
public void putTicketInCache(final CasAuthenticationToken token) {
final Element element = new Element(token.getCredentials().toString(), token);
if (logger.isDebugEnabled()) {
logger.debug("Cache put: " + element.getKey());
@ -85,7 +80,7 @@ public class EhCacheBasedTicketCache implements StatelessTicketCache, Initializi
cache.put(element);
}
public void removeTicketFromCache(CasAuthenticationToken token) {
public void removeTicketFromCache(final CasAuthenticationToken token) {
if (logger.isDebugEnabled()) {
logger.debug("Cache remove: " + token.getCredentials().toString());
}
@ -93,11 +88,11 @@ public class EhCacheBasedTicketCache implements StatelessTicketCache, Initializi
this.removeTicketFromCache(token.getCredentials().toString());
}
public void removeTicketFromCache(String serviceTicket) {
public void removeTicketFromCache(final String serviceTicket) {
cache.remove(serviceTicket);
}
public void setCache(Ehcache cache) {
public void setCache(final Ehcache cache) {
this.cache = cache;
}
}

View File

@ -0,0 +1,35 @@
package org.springframework.security.cas.userdetails;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.util.Assert;
import org.jasig.cas.client.validation.Assertion;
/**
* Abstract class for using the provided CAS assertion to construct a new User object. This generally is most
* useful when combined with a SAML-based response from the CAS Server/client.
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.0
*/
public abstract class AbstractCasAssertionUserDetailsService implements AuthenticationUserDetailsService {
public final UserDetails loadUserDetails(final Authentication token) throws UsernameNotFoundException {
Assert.isInstanceOf(CasAuthenticationToken.class, token, "The provided token MUST be an instance of CasAuthenticationToken.class");
return loadUserDetails(((CasAssertionAuthenticationToken) token).getAssertion());
}
/**
* Protected template method for construct a {@link org.springframework.security.core.userdetails.UserDetails} via the supplied CAS
* assertion.
*
* @param assertion the assertion to use to construct the new UserDetails. CANNOT be NULL.
* @return the newly constructed UserDetails.
*/
protected abstract UserDetails loadUserDetails(Assertion assertion);
}

View File

@ -0,0 +1,57 @@
package org.springframework.security.cas.userdetails;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.jasig.cas.client.validation.Assertion;
import java.util.List;
import java.util.ArrayList;
/**
* Populates the {@link org.springframework.security.core.GrantedAuthority}s for a user by reading a list of attributes that were returned as
* part of the CAS response. Each attribute is read and each value of the attribute is turned into a GrantedAuthority. If the attribute has no
* value then its not added.
*
* @author Scott Battaglia
* @version $Revision$ $Date$
* @since 3.0
*/
public final class GrantedAuthorityFromAssertionAttributesUserDetailsService extends AbstractCasAssertionUserDetailsService implements InitializingBean {
private String[] attributes;
@Override
protected UserDetails loadUserDetails(final Assertion assertion) {
final List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
for (final String attribute : this.attributes) {
final Object attributes = assertion.getPrincipal().getAttributes().get(attribute);
if (attributes == null) {
continue;
}
if (attributes instanceof List) {
final List list = (List) attributes;
for (final Object o : list) {
grantedAuthorities.add(new GrantedAuthorityImpl(o.toString()));
}
} else {
grantedAuthorities.add(new GrantedAuthorityImpl(attributes.toString()));
}
}
return new User(assertion.getPrincipal().getName(), null, true, true, true, true, grantedAuthorities);
}
public void afterPropertiesSet() throws Exception {
Assert.isTrue(attributes != null && attributes.length > 0, "At least one attribute is required to retrieve roles from.");
}
}

View File

@ -79,6 +79,8 @@ public class CasProcessingFilter extends AbstractAuthenticationProcessingFilter
*/
private ProxyGrantingTicketStorage proxyGrantingTicketStorage;
private String artifactParameter = ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER;
//~ Constructors ===================================================================================================
public CasProcessingFilter() {
@ -87,16 +89,16 @@ public class CasProcessingFilter extends AbstractAuthenticationProcessingFilter
//~ Methods ========================================================================================================
public Authentication attemptAuthentication(final HttpServletRequest request, HttpServletResponse response)
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
throws AuthenticationException {
final String username = CAS_STATEFUL_IDENTIFIER;
String password = request.getParameter("ticket");
String password = request.getParameter(this.artifactParameter);
if (password == null) {
password = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
final UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
@ -129,4 +131,8 @@ public class CasProcessingFilter extends AbstractAuthenticationProcessingFilter
final ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
this.proxyGrantingTicketStorage = proxyGrantingTicketStorage;
}
public final void setServiceProperties(final ServiceProperties serviceProperties) {
this.artifactParameter = serviceProperties.getArtifactParameter();
}
}

View File

@ -45,8 +45,8 @@ import org.springframework.util.Assert;
*/
public class CasProcessingFilterEntryPoint implements AuthenticationEntryPoint, InitializingBean {
//~ Instance fields ================================================================================================
private ServiceProperties serviceProperties;
private String loginUrl;
/**
@ -66,12 +66,11 @@ public class CasProcessingFilterEntryPoint implements AuthenticationEntryPoint,
Assert.notNull(this.serviceProperties, "serviceProperties must be specified");
}
public void commence(final HttpServletRequest servletRequest, final HttpServletResponse servletResponse,
public void commence(final HttpServletRequest servletRequest, final HttpServletResponse response,
final AuthenticationException authenticationException) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String urlEncodedService = CommonUtils.constructServiceUrl(null, response, this.serviceProperties.getService(), null, "ticket", this.encodeServiceUrlWithSessionId);
final String redirectUrl = CommonUtils.constructRedirectUrl(this.loginUrl, "service", urlEncodedService, this.serviceProperties.isSendRenew(), false);
final String urlEncodedService = CommonUtils.constructServiceUrl(null, response, this.serviceProperties.getService(), null, this.serviceProperties.getArtifactParameter(), this.encodeServiceUrlWithSessionId);
final String redirectUrl = CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), urlEncodedService, this.serviceProperties.isSendRenew(), false);
response.sendRedirect(redirectUrl);
}

View File

@ -29,10 +29,7 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.*;
import java.util.HashMap;
@ -77,7 +74,7 @@ public class CasAuthenticationProviderTests {
@Test
public void statefulAuthenticationIsSuccessful() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
StatelessTicketCache cache = new MockStatelessTicketCache();
@ -119,7 +116,7 @@ public class CasAuthenticationProviderTests {
@Test
public void statelessAuthenticationIsSuccessful() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
StatelessTicketCache cache = new MockStatelessTicketCache();
@ -158,7 +155,7 @@ public class CasAuthenticationProviderTests {
@Test(expected = BadCredentialsException.class)
public void missingTicketIdIsDetected() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
StatelessTicketCache cache = new MockStatelessTicketCache();
@ -177,7 +174,7 @@ public class CasAuthenticationProviderTests {
public void invalidKeyIsDetected() throws Exception {
final Assertion assertion = new AssertionImpl("test");
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
StatelessTicketCache cache = new MockStatelessTicketCache();
@ -205,7 +202,7 @@ public class CasAuthenticationProviderTests {
@Test(expected = IllegalArgumentException.class)
public void detectsMissingKey() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setStatelessTicketCache(new MockStatelessTicketCache());
cap.setTicketValidator(new MockTicketValidator(true));
cap.setServiceProperties(makeServiceProperties());
@ -217,7 +214,7 @@ public class CasAuthenticationProviderTests {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
// set this explicitly to null to test failure
cap.setStatelessTicketCache(null);
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setTicketValidator(new MockTicketValidator(true));
cap.setServiceProperties(makeServiceProperties());
@ -227,7 +224,7 @@ public class CasAuthenticationProviderTests {
@Test(expected = IllegalArgumentException.class)
public void detectsMissingTicketValidator() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setStatelessTicketCache(new MockStatelessTicketCache());
cap.setServiceProperties(makeServiceProperties());
@ -237,14 +234,15 @@ public class CasAuthenticationProviderTests {
@Test
public void gettersAndSettersMatch() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setStatelessTicketCache(new MockStatelessTicketCache());
cap.setTicketValidator(new MockTicketValidator(true));
cap.setServiceProperties(makeServiceProperties());
cap.afterPropertiesSet();
assertTrue(cap.getUserDetailsService() != null);
// TODO disabled because why do we need to expose this?
// assertTrue(cap.getUserDetailsService() != null);
assertEquals("qwerty", cap.getKey());
assertTrue(cap.getStatelessTicketCache() != null);
assertTrue(cap.getTicketValidator() != null);
@ -253,7 +251,7 @@ public class CasAuthenticationProviderTests {
@Test
public void ignoresClassesItDoesNotSupport() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setStatelessTicketCache(new MockStatelessTicketCache());
cap.setTicketValidator(new MockTicketValidator(true));
@ -271,7 +269,7 @@ public class CasAuthenticationProviderTests {
@Test
public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setUserDetailsService(new MockAuthoritiesPopulator());
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setStatelessTicketCache(new MockStatelessTicketCache());
cap.setTicketValidator(new MockTicketValidator(true));
@ -292,8 +290,9 @@ public class CasAuthenticationProviderTests {
//~ Inner Classes ==================================================================================================
private class MockAuthoritiesPopulator implements UserDetailsService {
public UserDetails loadUserByUsername(String casUserId) throws AuthenticationException {
private class MockAuthoritiesPopulator implements AuthenticationUserDetailsService {
public UserDetails loadUserDetails(final Authentication token) throws UsernameNotFoundException {
return makeUserDetailsFromAuthoritiesPopulator();
}
}