From 8d3a2b42d9c5036e67195b83f1167afb6e982434 Mon Sep 17 00:00:00 2001 From: Scott McCrory Date: Thu, 27 Jul 2006 01:13:46 +0000 Subject: [PATCH] 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. --- .../SiteminderAuthenticationProvider.java | 132 ++++++ .../providers/siteminder/package.html | 5 + ...eminderAuthenticationProcessingFilter.java | 132 ++---- .../org/acegisecurity/ui/webapp/package.html | 2 +- ...SiteminderAuthenticationProviderTests.java | 430 ++++++++++++++++++ ...erAuthenticationProcessingFilterTests.java | 33 +- doc/docbook/acegi.xml | 76 ++-- 7 files changed, 649 insertions(+), 161 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProvider.java create mode 100644 core/src/main/java/org/acegisecurity/providers/siteminder/package.html create mode 100644 core/src/test/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProviderTests.java diff --git a/core/src/main/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProvider.java new file mode 100644 index 0000000000..c4f7042975 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProvider.java @@ -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; + } + +} diff --git a/core/src/main/java/org/acegisecurity/providers/siteminder/package.html b/core/src/main/java/org/acegisecurity/providers/siteminder/package.html new file mode 100644 index 0000000000..7457c82eed --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/siteminder/package.html @@ -0,0 +1,5 @@ + + +A Siteminder authentication provider. + + diff --git a/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java index 2f3ce4573f..bd95cc48d2 100644 --- a/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java +++ b/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java @@ -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.

Also provides a - * backup form-based authentication and the ability set source key names.

- *

Siteminder must present two headers to this filter, a username and password. You must set the + * Extends Acegi's AuthenticationProcessingFilter to pick up CA/Netegrity Siteminder headers. + * + *

Also provides a backup form-based authentication and the ability set source key names.

+ * + *

Siteminder must present two headers 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.

- *

Login forms must present two parameters to this filter: a username and password. If not + * + *

Login forms must present two parameters 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}.

- *

Do not use this class directly. Instead, configure web.xml to use the {@link + * + *

Do not use this class directly. Instead, configure web.xml to use the {@link * org.acegisecurity.util.FilterToBeanProxy}.

+ * + * @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 Authentication request token to the - * AuthenticationManager + * AuthenticationManager (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. * diff --git a/core/src/main/java/org/acegisecurity/ui/webapp/package.html b/core/src/main/java/org/acegisecurity/ui/webapp/package.html index 1fd2d439e8..f37f81c844 100644 --- a/core/src/main/java/org/acegisecurity/ui/webapp/package.html +++ b/core/src/main/java/org/acegisecurity/ui/webapp/package.html @@ -1,5 +1,5 @@ -Authenticates users via a standard web form and HttpSession. +Authenticates users via HTTP properties, headers and session. diff --git a/core/src/test/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProviderTests.java new file mode 100644 index 0000000000..a8df07ec39 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/siteminder/SiteminderAuthenticationProviderTests.java @@ -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) { + } + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java index 88db18a340..afd6c0b50a 100644 --- a/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java +++ b/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java @@ -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); diff --git a/doc/docbook/acegi.xml b/doc/docbook/acegi.xml index f46608ff85..3049961963 100644 --- a/doc/docbook/acegi.xml +++ b/doc/docbook/acegi.xml @@ -2120,7 +2120,8 @@ if (obj instanceof UserDetails) { Associates. Acegi Security provides a filter, - SiteminderAuthenticationProcessingFilter) that can + SiteminderAuthenticationProcessingFilter and + provider, SiteminderAuthenticationProvider that can be used to process requests that have been pre-authenticated by Siteminder. This filter assumes that you're using Siteminder for authentication, and that you're using Acegi @@ -2128,13 +2129,14 @@ if (obj instanceof UserDetails) { for authorization is not yet directly supported by Acegi Security. - 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 - SM_USER - which identifies the authenticated - principal. Please refer to your organization's "single sign-on" group - for header details in your particular configuration. + 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 SM_USER - which + identifies the authenticated principal (please refer to your + organization's "single sign-on" group for header details in your + particular configuration). @@ -2142,9 +2144,9 @@ if (obj instanceof UserDetails) { 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 - UsernamePasswordAuthenticationToken that can later - on be sent to the DaoAuthenticationProvider. Let's + header discussed earlier. It will be responsible for generating a + UsernamePasswordAuthenticationToken that is later + sent to the SiteminderAuthenticationProvider. Let's look at an example: <bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.SiteminderAuthenticationProcessingFilter"> @@ -2153,51 +2155,37 @@ if (obj instanceof UserDetails) { <property name="defaultTargetUrl"><value>/security.do?method=getMainMenu</value></property> <property name="filterProcessesUrl"><value>/j_acegi_security_check</value></property> <property name="siteminderUsernameHeaderKey"><value>SM_USER</value></property> - <property name="siteminderPasswordHeaderKey"><value>SM_USER</value></property> + <property name="formUsernameParameterKey"><value>j_username</value></property> </bean> In our example above, the bean is being provided an AuthenticationManager, 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. + 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! Note that you'll need a - DaoAuthenticationProvider + SiteminderAuthenticationProvider configured against your ProviderManager in order to - use the Siteminder authentication mechanism. Normally a - DaoAuthenticationProvider expects the password + use the Siteminder authentication mechanism. Normally an + AuthenticationProvider expects the password property to match what it retrieves from the - UserDetailsSource. 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 DaoAuthenticationProvider 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 DaoAuthenticationProvider - should simply be to build the complete - Authentication object (ie with suitable + UserDetailsSource, 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 + UserDetailsService should simply be to build the + complete Authentication object (ie with suitable GrantedAuthority[]s). - Advanced tip and word to the wise: the - SiteminderAuthenticationProcessingFilter actually - extends AuthenticationProcessingFilter and thus - additionally supports form validation. If you configure the filter to - support both, and code your - daoAuthenticationProvider 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! - - TODO: We should write a dedicated - AuthenticationProvider rather than require users to - modify their DaoAuthenticationProvider. Also review - the mixed use of SiteminderAuthenticationProcessingFilter, as it's - inconsistent with the rest of Acegi Security's authentication - mechanisms which are high cohesion. + 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!