SEC-319: Improvements to Siteminder integration: Create its own authentication provider & reeval strategy. Note that documentation not yet complete, but code is functional, test-covered and validated in a Siteminder environment.

This commit is contained in:
Scott McCrory 2006-07-27 01:13:46 +00:00
parent 52a167acfa
commit 8d3a2b42d9
7 changed files with 649 additions and 161 deletions

View File

@ -0,0 +1,132 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed 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
*
* 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.
*/
package org.acegisecurity.providers.siteminder;
import org.acegisecurity.AccountExpiredException;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationServiceException;
import org.acegisecurity.CredentialsExpiredException;
import org.acegisecurity.DisabledException;
import org.acegisecurity.LockedException;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation that retrieves user details from an {@link UserDetailsService}.
*
* @author Scott McCrory
* @version $Id: SiteminderAuthenticationProvider.java 1582 2006-07-15 15:18:51Z smccrory $
*/
public class SiteminderAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* Our logging object
*/
private static final Log logger = LogFactory.getLog(SiteminderAuthenticationProvider.class);
//~ Instance fields ================================================================================================
/**
* Our user details service (which does the real work of checking the user against a back-end user store).
*/
private UserDetailsService userDetailsService;
//~ Methods ========================================================================================================
/**
* @see org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks(org.acegisecurity.userdetails.UserDetails, org.acegisecurity.providers.UsernamePasswordAuthenticationToken)
*/
protected void additionalAuthenticationChecks(final UserDetails user,
final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// No need for password authentication checks - we only expect one identifying string
// from the HTTP Request header (as populated by Siteminder), but we do need to see if
// the user's account is OK to let them in.
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
"Account disabled"));
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
"Account expired"));
}
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
"Account locked"));
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "Credentials expired"));
}
}
/**
* @see org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider#doAfterPropertiesSet()
*/
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
/**
* Return the user details service.
* @return The user details service.
*/
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
/**
* @see org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider#retrieveUser(java.lang.String, org.acegisecurity.providers.UsernamePasswordAuthenticationToken)
*/
protected final UserDetails retrieveUser(final String username,
final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
} catch (DataAccessException repositoryProblem) {
throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new AuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
/**
* Sets the user details service.
* @param userDetailsService The user details service.
*/
public void setUserDetailsService(final UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}

View File

@ -0,0 +1,5 @@
<html>
<body>
A Siteminder authentication provider.
</body>
</html>

View File

@ -15,36 +15,40 @@
package org.acegisecurity.ui.webapp; package org.acegisecurity.ui.webapp;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.context.HttpSessionContextIntegrationFilter;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/** /**
* Extends Acegi's AuthenticationProcessingFilter to pick up CA/Netegrity Siteminder headers.<P>Also provides a * Extends Acegi's AuthenticationProcessingFilter to pick up CA/Netegrity Siteminder headers.
* backup form-based authentication and the ability set source key names.</p> *
* <P>Also provides a backup form-based authentication and the ability set source key names.</p>
*
* <P><B>Siteminder</B> must present two <B>headers</B> to this filter, a username and password. You must set the * <P><B>Siteminder</B> must present two <B>headers</B> to this filter, a username and password. You must set the
* header keys before this filter is used for authentication, otherwise Siteminder checks will be skipped. If the * header keys before this filter is used for authentication, otherwise Siteminder checks will be skipped. If the
* Siteminder check is unsuccessful (i.e. if the headers are not found), then the form parameters will be checked (see * Siteminder check is unsuccessful (i.e. if the headers are not found), then the form parameters will be checked (see
* next paragraph). This allows applications to optionally function even when their Siteminder infrastructure is * next paragraph). This allows applications to optionally function even when their Siteminder infrastructure is
* unavailable, as is often the case during development.</p> * unavailable, as is often the case during development.</p>
*
* <P><B>Login forms</B> must present two <B>parameters</B> to this filter: a username and password. If not * <P><B>Login forms</B> must present two <B>parameters</B> to this filter: a username and password. If not
* specified, the parameter names to use are contained in the static fields {@link #ACEGI_SECURITY_FORM_USERNAME_KEY} * specified, the parameter names to use are contained in the static fields {@link #ACEGI_SECURITY_FORM_USERNAME_KEY}
* and {@link #ACEGI_SECURITY_FORM_PASSWORD_KEY}.</p> * and {@link #ACEGI_SECURITY_FORM_PASSWORD_KEY}.</p>
*
* <P><B>Do not use this class directly.</B> Instead, configure <code>web.xml</code> to use the {@link * <P><B>Do not use this class directly.</B> Instead, configure <code>web.xml</code> to use the {@link
* org.acegisecurity.util.FilterToBeanProxy}.</p> * org.acegisecurity.util.FilterToBeanProxy}.</p>
*
* @author Scott McCrory
* @version $Id$
*/ */
public class SiteminderAuthenticationProcessingFilter extends AuthenticationProcessingFilter { public class SiteminderAuthenticationProcessingFilter extends AuthenticationProcessingFilter {
//~ Static fields/initializers ===================================================================================== //~ Static fields/initializers =====================================================================================
/** Log instance for debugging */ /** Log instance for debugging */
@ -52,15 +56,9 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
//~ Instance fields ================================================================================================ //~ Instance fields ================================================================================================
/** Form password request key. */
private String formPasswordParameterKey = null;
/** Form username request key. */ /** Form username request key. */
private String formUsernameParameterKey = null; private String formUsernameParameterKey = null;
/** Siteminder password header key. */
private String siteminderPasswordHeaderKey = null;
/** Siteminder username header key. */ /** Siteminder username header key. */
private String siteminderUsernameHeaderKey = null; private String siteminderUsernameHeaderKey = null;
@ -76,24 +74,19 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
/** /**
*
* @see org.acegisecurity.ui.AbstractProcessingFilter#attemptAuthentication(javax.servlet.http.HttpServletRequest) * @see org.acegisecurity.ui.AbstractProcessingFilter#attemptAuthentication(javax.servlet.http.HttpServletRequest)
*/ */
public Authentication attemptAuthentication(HttpServletRequest request) public Authentication attemptAuthentication(final HttpServletRequest request) throws AuthenticationException {
throws AuthenticationException {
String username = null;
String password = null;
// Check the Siteminder headers for authentication info String username = null;
if ((siteminderUsernameHeaderKey != null) && (siteminderUsernameHeaderKey.length() > 0)
&& (siteminderPasswordHeaderKey != null) && (siteminderPasswordHeaderKey.length() > 0)) { // Check the Siteminder header for identification info
if ((siteminderUsernameHeaderKey != null) && (siteminderUsernameHeaderKey.length() > 0)) {
username = request.getHeader(siteminderUsernameHeaderKey); username = request.getHeader(siteminderUsernameHeaderKey);
password = request.getHeader(siteminderPasswordHeaderKey);
} }
// If the Siteminder authentication info wasn't available, then get it // If the Siteminder identification info wasn't available, then try to get it from the form
// from the form parameters if ((username == null) || (username.length() == 0)) {
if ((username == null) || (username.length() == 0) || (password == null) || (password.length() == 0)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Siteminder headers not found for authentication, so trying to use form values"); logger.debug("Siteminder headers not found for authentication, so trying to use form values");
} }
@ -104,7 +97,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
username = request.getParameter(ACEGI_SECURITY_FORM_USERNAME_KEY); username = request.getParameter(ACEGI_SECURITY_FORM_USERNAME_KEY);
} }
password = obtainPassword(request);
} }
// Convert username and password to upper case. This is normally not a // Convert username and password to upper case. This is normally not a
@ -117,14 +109,9 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
username = ""; username = "";
} }
if (password != null) { // Pass in a null password value because it isn't relevant for Siteminder.
password = password.toUpperCase(); // Of course the AuthenticationManager needs to not care!
} else { UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, null);
// If password is null, set to blank to avoid a NPE.
password = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property // Allow subclasses to set the "details" property
setDetails(request, authRequest); setDetails(request, authRequest);
@ -135,15 +122,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
return this.getAuthenticationManager().authenticate(authRequest); return this.getAuthenticationManager().authenticate(authRequest);
} }
/**
* Returns the form password parameter key.
*
* @return The form password parameter key.
*/
public String getFormPasswordParameterKey() {
return formPasswordParameterKey;
}
/** /**
* Returns the form username parameter key. * Returns the form username parameter key.
* *
@ -153,15 +131,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
return formUsernameParameterKey; return formUsernameParameterKey;
} }
/**
* Returns the Siteminder password header key.
*
* @return The Siteminder password header key.
*/
public String getSiteminderPasswordHeaderKey() {
return siteminderPasswordHeaderKey;
}
/** /**
* Returns the Siteminder username header key. * Returns the Siteminder username header key.
* *
@ -172,20 +141,14 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
} }
/** /**
* Overridden method to obtain different value depending on whether Siteminder or form validation is being * Overridden method to always return a null (Siteminder doesn't pass on the password).
* performed.
* *
* @param request so that request attributes can be retrieved * @param request so that request attributes can be retrieved
*
* @return the password that will be presented in the <code>Authentication</code> request token to the * @return the password that will be presented in the <code>Authentication</code> request token to the
* <code>AuthenticationManager</code> * <code>AuthenticationManager</code> (null).
*/ */
protected String obtainPassword(HttpServletRequest request) { protected String obtainPassword(final HttpServletRequest request) {
if ((formPasswordParameterKey != null) && (formPasswordParameterKey.length() > 0)) { return null;
return request.getParameter(formPasswordParameterKey);
} else {
return request.getParameter(ACEGI_SECURITY_FORM_PASSWORD_KEY);
}
} }
/** /**
@ -197,6 +160,7 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
* javax.servlet.http.HttpServletResponse) * javax.servlet.http.HttpServletResponse)
*/ */
protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) { protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';'); int pathParamIndex = uri.indexOf(';');
@ -208,8 +172,8 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
//attempt authentication if j_secuity_check is present or if the getDefaultTargetUrl() //attempt authentication if j_secuity_check is present or if the getDefaultTargetUrl()
//is present and user is not already authenticated. //is present and user is not already authenticated.
boolean bAuthenticated = false; boolean bAuthenticated = false;
SecurityContext context = (SecurityContext) request.getSession() SecurityContext context = (SecurityContext) request.getSession().getAttribute(
.getAttribute(HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY); HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY);
if (context != null) { if (context != null) {
Authentication auth = context.getAuthentication(); Authentication auth = context.getAuthentication();
@ -231,15 +195,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
return bAttemptAuthentication; return bAttemptAuthentication;
} }
/**
* Sets the form password parameter key.
*
* @param key The form password parameter key.
*/
public void setFormPasswordParameterKey(final String key) {
this.formPasswordParameterKey = key;
}
/** /**
* Sets the form username parameter key. * Sets the form username parameter key.
* *
@ -249,15 +204,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
this.formUsernameParameterKey = key; this.formUsernameParameterKey = key;
} }
/**
* Sets the Siteminder password header key.
*
* @param key The Siteminder password header key.
*/
public void setSiteminderPasswordHeaderKey(final String key) {
this.siteminderPasswordHeaderKey = key;
}
/** /**
* Sets the Siteminder username header key. * Sets the Siteminder username header key.
* *

View File

@ -1,5 +1,5 @@
<html> <html>
<body> <body>
Authenticates users via a standard web form and <code>HttpSession</code>. Authenticates users via HTTP properties, headers and session.
</body> </body>
</html> </html>

View File

@ -0,0 +1,430 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed 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
*
* 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.
*/
package org.acegisecurity.providers.siteminder;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import org.acegisecurity.AccountExpiredException;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationServiceException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.CredentialsExpiredException;
import org.acegisecurity.DisabledException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.LockedException;
import org.acegisecurity.providers.TestingAuthenticationToken;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.UserCache;
import org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache;
import org.acegisecurity.providers.dao.cache.NullUserCache;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataRetrievalFailureException;
/**
* Tests {@link SiteminderAuthenticationProvider}.
*
* @author Ben Alex
* @version $Id: SiteminderAuthenticationProviderTests.java 1582 2006-07-15 15:18:51Z smccrory $
*/
public class SiteminderAuthenticationProviderTests extends TestCase {
//~ Methods ========================================================================================================
public static void main(String[] args) {
junit.textui.TestRunner.run(SiteminderAuthenticationProviderTests.class);
}
public final void setUp() throws Exception {
super.setUp();
}
public void testAuthenticateFailsIfAccountExpired() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserPeterAccountExpired());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown AccountExpiredException");
} catch (AccountExpiredException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsIfAccountLocked() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserPeterAccountLocked());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown LockedException");
} catch (LockedException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsIfCredentialsExpired() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserPeterCredentialsExpired());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown CredentialsExpiredException");
} catch (CredentialsExpiredException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsIfUserDisabled() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserPeter());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown DisabledException");
} catch (DisabledException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsWhenUserDetailsServiceHasBackendFailure() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceSimulateBackendError());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown AuthenticationServiceException");
} catch (AuthenticationServiceException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsWithEmptyUsername() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(null, "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
} catch (BadCredentialsException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionFalse() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false); // we want UsernameNotFoundExceptions
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown UsernameNotFoundException");
} catch (UsernameNotFoundException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionsWithDefaultOfTrue() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("INVALID_USER", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
assertTrue(provider.isHideUserNotFoundExceptions());
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
} catch (BadCredentialsException expected) {
assertTrue(true);
}
}
public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("MaRiSSA", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
} catch (BadCredentialsException expected) {
assertTrue(true);
}
}
public void testAuthenticates() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", "koala");
token.setDetails("192.168.0.1");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result;
assertEquals(User.class, castResult.getPrincipal().getClass());
assertEquals("koala", castResult.getCredentials());
assertEquals("ROLE_ONE", castResult.getAuthorities()[0].getAuthority());
assertEquals("ROLE_TWO", castResult.getAuthorities()[1].getAuthority());
assertEquals("192.168.0.1", castResult.getDetails());
}
public void testAuthenticatesASecondTime() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
// Now try to authenticate with the previous result (with its UserDetails)
Authentication result2 = provider.authenticate(result);
if (!(result2 instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
assertEquals(result.getCredentials(), result2.getCredentials());
}
public void testAuthenticatesWithForcePrincipalAsString() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
provider.setUserCache(new MockUserCache());
provider.setForcePrincipalAsString(true);
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result;
assertEquals(String.class, castResult.getPrincipal().getClass());
assertEquals("marissa", castResult.getPrincipal());
}
public void testDetectsNullBeingReturnedFromUserDetailsService() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("marissa", "koala");
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceReturnsNull());
try {
provider.authenticate(token);
fail("Should have thrown AuthenticationServiceException");
} catch (AuthenticationServiceException expected) {
assertEquals("UserDetailsService returned null, which is an interface contract violation", expected
.getMessage());
}
}
public void testGettersSetters() {
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserCache(new EhCacheBasedUserCache());
assertEquals(EhCacheBasedUserCache.class, provider.getUserCache().getClass());
assertFalse(provider.isForcePrincipalAsString());
provider.setForcePrincipalAsString(true);
assertTrue(provider.isForcePrincipalAsString());
}
public void testStartupFailsIfNoUserDetailsService() throws Exception {
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
try {
provider.afterPropertiesSet();
fail("Should have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertTrue(true);
}
}
public void testStartupFailsIfNoUserCacheSet() throws Exception {
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
provider.setUserDetailsService(new MockUserDetailsServiceUserMarissa());
assertEquals(NullUserCache.class, provider.getUserCache().getClass());
provider.setUserCache(null);
try {
provider.afterPropertiesSet();
fail("Should have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertTrue(true);
}
}
public void testStartupSuccess() throws Exception {
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
UserDetailsService userDetailsService = new MockUserDetailsServiceUserMarissa();
provider.setUserDetailsService(userDetailsService);
provider.setUserCache(new MockUserCache());
assertEquals(userDetailsService, provider.getUserDetailsService());
provider.afterPropertiesSet();
assertTrue(true);
}
public void testSupports() {
SiteminderAuthenticationProvider provider = new SiteminderAuthenticationProvider();
assertTrue(provider.supports(UsernamePasswordAuthenticationToken.class));
assertTrue(!provider.supports(TestingAuthenticationToken.class));
}
//~ Inner Classes ==================================================================================================
private class MockUserDetailsServiceReturnsNull implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
return null;
}
}
private class MockUserDetailsServiceSimulateBackendError implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
throw new DataRetrievalFailureException("This mock simulator is designed to fail");
}
}
private class MockUserDetailsServiceUserMarissa implements UserDetailsService {
private String password = "koala";
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("marissa".equals(username)) {
return new User("marissa", password, true, true, true, true, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
public void setPassword(String password) {
this.password = password;
}
}
private class MockUserDetailsServiceUserMarissaWithSalt implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("marissa".equals(username)) {
return new User("marissa", "koala{SYSTEM_SALT_VALUE}", true, true, true, true, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockUserDetailsServiceUserPeter implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("peter".equals(username)) {
return new User("peter", "opal", false, true, true, true, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockUserDetailsServiceUserPeterAccountExpired implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("peter".equals(username)) {
return new User("peter", "opal", true, false, true, true, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockUserDetailsServiceUserPeterAccountLocked implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("peter".equals(username)) {
return new User("peter", "opal", true, true, true, false, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockUserDetailsServiceUserPeterCredentialsExpired implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
if ("peter".equals(username)) {
return new User("peter", "opal", true, true, false, true, new GrantedAuthority[] {
new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl("ROLE_TWO") });
} else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockUserCache implements UserCache {
private Map cache = new HashMap();
public UserDetails getUserFromCache(String username) {
return (User) cache.get(username);
}
public void putUserInCache(UserDetails user) {
cache.put(user.getUsername(), user);
}
public void removeUserFromCache(String username) {
}
}
}

View File

@ -19,13 +19,10 @@ import junit.framework.TestCase;
import org.acegisecurity.Authentication; import org.acegisecurity.Authentication;
import org.acegisecurity.MockAuthenticationManager; import org.acegisecurity.MockAuthenticationManager;
import org.acegisecurity.ui.WebAuthenticationDetails; import org.acegisecurity.ui.WebAuthenticationDetails;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
/** /**
* Tests SiteminderAuthenticationProcessingFilter. * Tests SiteminderAuthenticationProcessingFilter.
* *
@ -92,15 +89,9 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
filter.setFilterProcessesUrl("foobar"); filter.setFilterProcessesUrl("foobar");
assertEquals("foobar", filter.getFilterProcessesUrl()); assertEquals("foobar", filter.getFilterProcessesUrl());
filter.setFormPasswordParameterKey("passwordParamKey");
assertEquals("passwordParamKey", filter.getFormPasswordParameterKey());
filter.setFormUsernameParameterKey("usernameParamKey"); filter.setFormUsernameParameterKey("usernameParamKey");
assertEquals("usernameParamKey", filter.getFormUsernameParameterKey()); assertEquals("usernameParamKey", filter.getFormUsernameParameterKey());
filter.setSiteminderPasswordHeaderKey("passwordHeaderKey");
assertEquals("passwordHeaderKey", filter.getSiteminderPasswordHeaderKey());
filter.setSiteminderUsernameHeaderKey("usernameHeaderKey"); filter.setSiteminderUsernameHeaderKey("usernameHeaderKey");
assertEquals("usernameHeaderKey", filter.getSiteminderUsernameHeaderKey()); assertEquals("usernameHeaderKey", filter.getSiteminderUsernameHeaderKey());
} }
@ -131,8 +122,7 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
* *
* @throws Exception * @throws Exception
*/ */
public void testFormNullPasswordHandledGracefully() public void testFormNullPasswordHandledGracefully() throws Exception {
throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY, "marissa"); request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY, "marissa");
@ -151,8 +141,7 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
* *
* @throws Exception * @throws Exception
*/ */
public void testFormNullUsernameHandledGracefully() public void testFormNullUsernameHandledGracefully() throws Exception {
throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_PASSWORD_KEY, "koala"); request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_PASSWORD_KEY, "koala");
@ -186,7 +175,6 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
filter.setAuthenticationManager(authMgrThatGrantsAccess); filter.setAuthenticationManager(authMgrThatGrantsAccess);
filter.setSiteminderUsernameHeaderKey("SM_USER"); filter.setSiteminderUsernameHeaderKey("SM_USER");
filter.setSiteminderPasswordHeaderKey("SM_USER");
filter.init(null); filter.init(null);
// Requests for an unknown URL should NOT require (re)authentication // Requests for an unknown URL should NOT require (re)authentication
@ -220,7 +208,6 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter();
filter.setAuthenticationManager(authMgr); filter.setAuthenticationManager(authMgr);
filter.setSiteminderUsernameHeaderKey("SM_USER"); filter.setSiteminderUsernameHeaderKey("SM_USER");
filter.setSiteminderPasswordHeaderKey("SM_USER");
filter.init(null); filter.init(null);
Authentication result = filter.attemptAuthentication(request); Authentication result = filter.attemptAuthentication(request);

View File

@ -2120,7 +2120,8 @@ if (obj instanceof UserDetails) {
Associates.</para> Associates.</para>
<para>Acegi Security provides a filter, <para>Acegi Security provides a filter,
<literal>SiteminderAuthenticationProcessingFilter</literal>) that can <literal>SiteminderAuthenticationProcessingFilter</literal> and
provider, <literal>SiteminderAuthenticationProvider</literal> that can
be used to process requests that have been pre-authenticated by be used to process requests that have been pre-authenticated by
Siteminder. This filter assumes that you're using Siteminder for Siteminder. This filter assumes that you're using Siteminder for
<emphasis>authentication</emphasis>, and that you're using Acegi <emphasis>authentication</emphasis>, and that you're using Acegi
@ -2128,13 +2129,14 @@ if (obj instanceof UserDetails) {
for <emphasis>authorization</emphasis> is not yet directly supported for <emphasis>authorization</emphasis> is not yet directly supported
by Acegi Security.</para> by Acegi Security.</para>
<para>In Siteminder, an agent is setup on your web server to intercept <para>When using Siteminder, an agent is setup on your web server to
a principal's first call to your application. The agent redirect the intercept a principal's first call to your application. The agent
web request to a single sign on login page, and then your application redirects the web request to a single sign-on login page, and once
receives the request. Inside the HTTP headers is a header - such as authenticated, your application receives the request. Inside the HTTP
<literal>SM_USER</literal> - which identifies the authenticated request is a header - such as <literal>SM_USER</literal> - which
principal. Please refer to your organization's "single sign-on" group identifies the authenticated principal (please refer to your
for header details in your particular configuration.</para> organization's "single sign-on" group for header details in your
particular configuration).</para>
</sect1> </sect1>
<sect1 id="siteminder-config"> <sect1 id="siteminder-config">
@ -2142,9 +2144,9 @@ if (obj instanceof UserDetails) {
<para>The first step in setting up Acegi Security's Siteminder support <para>The first step in setting up Acegi Security's Siteminder support
is to define the authentication mechanism that will inspect the HTTP is to define the authentication mechanism that will inspect the HTTP
header discussed earlier. It will then generate a header discussed earlier. It will be responsible for generating a
<literal>UsernamePasswordAuthenticationToken</literal> that can later <literal>UsernamePasswordAuthenticationToken</literal> that is later
on be sent to the <literal>DaoAuthenticationProvider</literal>. Let's sent to the <literal>SiteminderAuthenticationProvider</literal>. Let's
look at an example:</para> look at an example:</para>
<para><programlisting>&lt;bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.SiteminderAuthenticationProcessingFilter"&gt; <para><programlisting>&lt;bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.SiteminderAuthenticationProcessingFilter"&gt;
@ -2153,51 +2155,37 @@ if (obj instanceof UserDetails) {
&lt;property name="defaultTargetUrl"&gt;&lt;value&gt;/security.do?method=getMainMenu&lt;/value&gt;&lt;/property&gt; &lt;property name="defaultTargetUrl"&gt;&lt;value&gt;/security.do?method=getMainMenu&lt;/value&gt;&lt;/property&gt;
&lt;property name="filterProcessesUrl"&gt;&lt;value&gt;/j_acegi_security_check&lt;/value&gt;&lt;/property&gt; &lt;property name="filterProcessesUrl"&gt;&lt;value&gt;/j_acegi_security_check&lt;/value&gt;&lt;/property&gt;
&lt;property name="siteminderUsernameHeaderKey"&gt;&lt;value&gt;SM_USER&lt;/value&gt;&lt;/property&gt; &lt;property name="siteminderUsernameHeaderKey"&gt;&lt;value&gt;SM_USER&lt;/value&gt;&lt;/property&gt;
&lt;property name="siteminderPasswordHeaderKey"&gt;&lt;value&gt;SM_USER&lt;/value&gt;&lt;/property&gt; &lt;property name="formUsernameParameterKey"&gt;&lt;value&gt;j_username&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;</programlisting></para> &lt;/bean&gt;</programlisting></para>
<para>In our example above, the bean is being provided an <para>In our example above, the bean is being provided an
<literal>AuthenticationManager</literal>, as is normally needed by <literal>AuthenticationManager</literal>, as is normally needed by
authentication mechanisms. Several URLs are also specified, with the authentication mechanisms. Several URLs are also specified, with the
values being self-explanatory. It's important to also specify the HTTP values being self-explanatory. It's important to also specify the HTTP
headers that Acegi Security should inspect. Most people won't need the header that Acegi Security should inspect. If you additionally want to
password value since Siteminder has already authenticated the user, so support form-based authentication (i.e. in your development
it's typical to use the same header for both.</para> environment where Siteminder is not installed), specify the form's
username parameter as well - just don't do this in production!</para>
<para>Note that you'll need a <para>Note that you'll need a
<literal><literal>DaoAuthenticationProvider</literal></literal> <literal><literal>SiteminderAuthenticationProvider</literal></literal>
configured against your <literal>ProviderManager</literal> in order to configured against your <literal>ProviderManager</literal> in order to
use the Siteminder authentication mechanism. Normally a use the Siteminder authentication mechanism. Normally an
<literal>DaoAuthenticationProvider</literal> expects the password <literal>AuthenticationProvider</literal> expects the password
property to match what it retrieves from the property to match what it retrieves from the
<literal>UserDetailsSource</literal>. In this case, authentication has <literal>UserDetailsSource</literal>, but in this case, authentication
already been handled by Siteminder and you've specified the same HTTP has already been handled by Siteminder, so password property is not
header for both username and password. As such, you must modify the even relevant. This may sound like a security weakness, but remember
code of <literal>DaoAuthenticationProvider</literal> to simply make that users have to authenticate with Siteminder before your
sure the username and password values match. This may sound like a application ever receives the requests, so the purpose of your custom
security weakness, but remember that users have to authenticate with <literal>UserDetailsService</literal> should simply be to build the
Siteminder before your application ever receives the requests, so the complete <literal>Authentication</literal> object (ie with suitable
purpose of your custom <literal>DaoAuthenticationProvider</literal>
should simply be to build the complete
<literal>Authentication</literal> object (ie with suitable
<literal>GrantedAuthority[]</literal>s).</para> <literal>GrantedAuthority[]</literal>s).</para>
<para>Advanced tip and word to the wise: the <para>Advanced tip and word to the wise: If you additionally want to
<literal>SiteminderAuthenticationProcessingFilter</literal> actually support form-based authentication in your development environment
extends <literal>AuthenticationProcessingFilter</literal> and thus (where Siteminder is typically not installed), specify the form's
additionally supports form validation. If you configure the filter to username parameter as well. Just don't do this in production!</para>
support both, and code your
<literal>daoAuthenticationProvider</literal> to match the username and
passwords as described above, you'll potentially defeat any security
you have in place if the web server's Siteminder agent is deactivated.
Don't do this, especially in production!</para>
<para>TODO: We should write a dedicated
<literal>AuthenticationProvider</literal> rather than require users to
modify their <literal>DaoAuthenticationProvider</literal>. Also review
the mixed use of SiteminderAuthenticationProcessingFilter, as it's
inconsistent with the rest of Acegi Security's authentication
mechanisms which are high cohesion.</para>
</sect1> </sect1>
</chapter> </chapter>