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;
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.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
* 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
* Extends Acegi's AuthenticationProcessingFilter to pick up CA/Netegrity Siteminder headers.
*
* <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
* 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
* next paragraph). This allows applications to optionally function even when their Siteminder infrastructure is
* 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}
* 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>
*
* @author Scott McCrory
* @version $Id$
*/
public class SiteminderAuthenticationProcessingFilter extends AuthenticationProcessingFilter {
//~ Static fields/initializers =====================================================================================
/** Log instance for debugging */
@ -52,21 +56,15 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
//~ Instance fields ================================================================================================
/** Form password request key. */
private String formPasswordParameterKey = null;
/** Form username request key. */
private String formUsernameParameterKey = null;
/** Siteminder password header key. */
private String siteminderPasswordHeaderKey = null;
/** Siteminder username header key. */
private String siteminderUsernameHeaderKey = null;
//~ Constructors ===================================================================================================
/**
/**
* Basic constructor.
*/
public SiteminderAuthenticationProcessingFilter() {
@ -76,24 +74,19 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
//~ Methods ========================================================================================================
/**
*
* @see org.acegisecurity.ui.AbstractProcessingFilter#attemptAuthentication(javax.servlet.http.HttpServletRequest)
*/
public Authentication attemptAuthentication(HttpServletRequest request)
throws AuthenticationException {
String username = null;
String password = null;
public Authentication attemptAuthentication(final HttpServletRequest request) throws AuthenticationException {
// Check the Siteminder headers for authentication info
if ((siteminderUsernameHeaderKey != null) && (siteminderUsernameHeaderKey.length() > 0)
&& (siteminderPasswordHeaderKey != null) && (siteminderPasswordHeaderKey.length() > 0)) {
String username = null;
// Check the Siteminder header for identification info
if ((siteminderUsernameHeaderKey != null) && (siteminderUsernameHeaderKey.length() > 0)) {
username = request.getHeader(siteminderUsernameHeaderKey);
password = request.getHeader(siteminderPasswordHeaderKey);
}
// If the Siteminder authentication info wasn't available, then get it
// from the form parameters
if ((username == null) || (username.length() == 0) || (password == null) || (password.length() == 0)) {
// If the Siteminder identification info wasn't available, then try to get it from the form
if ((username == null) || (username.length() == 0)) {
if (logger.isDebugEnabled()) {
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);
}
password = obtainPassword(request);
}
// Convert username and password to upper case. This is normally not a
@ -117,14 +109,9 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
username = "";
}
if (password != null) {
password = password.toUpperCase();
} else {
// If password is null, set to blank to avoid a NPE.
password = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Pass in a null password value because it isn't relevant for Siteminder.
// Of course the AuthenticationManager needs to not care!
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, null);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
@ -135,15 +122,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
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.
*
@ -153,15 +131,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
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.
*
@ -172,20 +141,14 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
}
/**
* Overridden method to obtain different value depending on whether Siteminder or form validation is being
* performed.
* Overridden method to always return a null (Siteminder doesn't pass on the password).
*
* @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
* <code>AuthenticationManager</code>
* <code>AuthenticationManager</code> (null).
*/
protected String obtainPassword(HttpServletRequest request) {
if ((formPasswordParameterKey != null) && (formPasswordParameterKey.length() > 0)) {
return request.getParameter(formPasswordParameterKey);
} else {
return request.getParameter(ACEGI_SECURITY_FORM_PASSWORD_KEY);
}
protected String obtainPassword(final HttpServletRequest request) {
return null;
}
/**
@ -197,6 +160,7 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
* javax.servlet.http.HttpServletResponse)
*/
protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
String uri = request.getRequestURI();
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()
//is present and user is not already authenticated.
boolean bAuthenticated = false;
SecurityContext context = (SecurityContext) request.getSession()
.getAttribute(HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY);
SecurityContext context = (SecurityContext) request.getSession().getAttribute(
HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY);
if (context != null) {
Authentication auth = context.getAuthentication();
@ -222,7 +186,7 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
// if true is returned then authentication will be attempted.
boolean bAttemptAuthentication = (uri.endsWith(request.getContextPath() + getFilterProcessesUrl()))
|| ((getDefaultTargetUrl() != null) && uri.endsWith(getDefaultTargetUrl()) && !bAuthenticated);
|| ((getDefaultTargetUrl() != null) && uri.endsWith(getDefaultTargetUrl()) && !bAuthenticated);
if (logger.isDebugEnabled()) {
logger.debug("Authentication attempted for the following URI ==> " + uri + " is " + bAttemptAuthentication);
@ -231,15 +195,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
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.
*
@ -249,15 +204,6 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc
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.
*

View File

@ -1,5 +1,5 @@
<html>
<body>
Authenticates users via a standard web form and <code>HttpSession</code>.
Authenticates users via HTTP properties, headers and session.
</body>
</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.MockAuthenticationManager;
import org.acegisecurity.ui.WebAuthenticationDetails;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* Tests SiteminderAuthenticationProcessingFilter.
*
@ -36,18 +33,18 @@ import org.springframework.mock.web.MockHttpServletResponse;
public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
//~ Constructors ===================================================================================================
/**
* Basic constructor.
*/
/**
* Basic constructor.
*/
public SiteminderAuthenticationProcessingFilterTests() {
super();
}
/**
* Argument constructor.
*
* @param arg0
*/
/**
* Argument constructor.
*
* @param arg0
*/
public SiteminderAuthenticationProcessingFilterTests(String arg0) {
super(arg0);
}
@ -92,15 +89,9 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
filter.setFilterProcessesUrl("foobar");
assertEquals("foobar", filter.getFilterProcessesUrl());
filter.setFormPasswordParameterKey("passwordParamKey");
assertEquals("passwordParamKey", filter.getFormPasswordParameterKey());
filter.setFormUsernameParameterKey("usernameParamKey");
assertEquals("usernameParamKey", filter.getFormUsernameParameterKey());
filter.setSiteminderPasswordHeaderKey("passwordHeaderKey");
assertEquals("passwordHeaderKey", filter.getSiteminderPasswordHeaderKey());
filter.setSiteminderUsernameHeaderKey("usernameHeaderKey");
assertEquals("usernameHeaderKey", filter.getSiteminderUsernameHeaderKey());
}
@ -131,8 +122,7 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
*
* @throws Exception
*/
public void testFormNullPasswordHandledGracefully()
throws Exception {
public void testFormNullPasswordHandledGracefully() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY, "marissa");
@ -151,8 +141,7 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
*
* @throws Exception
*/
public void testFormNullUsernameHandledGracefully()
throws Exception {
public void testFormNullUsernameHandledGracefully() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addParameter(SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_PASSWORD_KEY, "koala");
@ -186,7 +175,6 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
filter.setAuthenticationManager(authMgrThatGrantsAccess);
filter.setSiteminderUsernameHeaderKey("SM_USER");
filter.setSiteminderPasswordHeaderKey("SM_USER");
filter.init(null);
// Requests for an unknown URL should NOT require (re)authentication
@ -220,7 +208,6 @@ public class SiteminderAuthenticationProcessingFilterTests extends TestCase {
SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter();
filter.setAuthenticationManager(authMgr);
filter.setSiteminderUsernameHeaderKey("SM_USER");
filter.setSiteminderPasswordHeaderKey("SM_USER");
filter.init(null);
Authentication result = filter.attemptAuthentication(request);

View File

@ -2120,7 +2120,8 @@ if (obj instanceof UserDetails) {
Associates.</para>
<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
Siteminder. This filter assumes that you're using Siteminder for
<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
by Acegi Security.</para>
<para>In Siteminder, an agent is setup on your web server to intercept
a principal's first call to your application. The agent redirect the
web request to a single sign on login page, and then your application
receives the request. Inside the HTTP headers is a header - such as
<literal>SM_USER</literal> - which identifies the authenticated
principal. Please refer to your organization's "single sign-on" group
for header details in your particular configuration.</para>
<para>When using Siteminder, an agent is setup on your web server to
intercept a principal's first call to your application. The agent
redirects the web request to a single sign-on login page, and once
authenticated, your application receives the request. Inside the HTTP
request is a header - such as <literal>SM_USER</literal> - which
identifies the authenticated principal (please refer to your
organization's "single sign-on" group for header details in your
particular configuration).</para>
</sect1>
<sect1 id="siteminder-config">
@ -2142,9 +2144,9 @@ if (obj instanceof UserDetails) {
<para>The first step in setting up Acegi Security's Siteminder support
is to define the authentication mechanism that will inspect the HTTP
header discussed earlier. It will then generate a
<literal>UsernamePasswordAuthenticationToken</literal> that can later
on be sent to the <literal>DaoAuthenticationProvider</literal>. Let's
header discussed earlier. It will be responsible for generating a
<literal>UsernamePasswordAuthenticationToken</literal> that is later
sent to the <literal>SiteminderAuthenticationProvider</literal>. Let's
look at an example:</para>
<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="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="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>
<para>In our example above, the bean is being provided an
<literal>AuthenticationManager</literal>, as is normally needed by
authentication mechanisms. Several URLs are also specified, with the
values being self-explanatory. It's important to also specify the HTTP
headers that Acegi Security should inspect. Most people won't need the
password value since Siteminder has already authenticated the user, so
it's typical to use the same header for both.</para>
header that Acegi Security should inspect. If you additionally want to
support form-based authentication (i.e. in your development
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
<literal><literal>DaoAuthenticationProvider</literal></literal>
<literal><literal>SiteminderAuthenticationProvider</literal></literal>
configured against your <literal>ProviderManager</literal> in order to
use the Siteminder authentication mechanism. Normally a
<literal>DaoAuthenticationProvider</literal> expects the password
use the Siteminder authentication mechanism. Normally an
<literal>AuthenticationProvider</literal> expects the password
property to match what it retrieves from the
<literal>UserDetailsSource</literal>. In this case, authentication has
already been handled by Siteminder and you've specified the same HTTP
header for both username and password. As such, you must modify the
code of <literal>DaoAuthenticationProvider</literal> to simply make
sure the username and password values match. This may sound like a
security weakness, but remember that users have to authenticate with
Siteminder before your application ever receives the requests, so the
purpose of your custom <literal>DaoAuthenticationProvider</literal>
should simply be to build the complete
<literal>Authentication</literal> object (ie with suitable
<literal>UserDetailsSource</literal>, but in this case, authentication
has already been handled by Siteminder, so password property is not
even relevant. This may sound like a security weakness, but remember
that users have to authenticate with Siteminder before your
application ever receives the requests, so the purpose of your custom
<literal>UserDetailsService</literal> should simply be to build the
complete <literal>Authentication</literal> object (ie with suitable
<literal>GrantedAuthority[]</literal>s).</para>
<para>Advanced tip and word to the wise: the
<literal>SiteminderAuthenticationProcessingFilter</literal> actually
extends <literal>AuthenticationProcessingFilter</literal> and thus
additionally supports form validation. If you configure the filter to
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>
<para>Advanced tip and word to the wise: If you additionally want to
support form-based authentication in your development environment
(where Siteminder is typically not installed), specify the form's
username parameter as well. Just don't do this in production!</para>
</sect1>
</chapter>