Procházet zdrojové kódy

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.

Scott McCrory před 19 roky
rodič
revize
8d3a2b42d9

+ 132 - 0
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;
+    }
+
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/providers/siteminder/package.html

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

+ 34 - 88
core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java

@@ -15,36 +15,40 @@
 
 package org.acegisecurity.ui.webapp;
 
+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;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-
 /**
- * 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 {
+    public Authentication attemptAuthentication(final HttpServletRequest request) throws AuthenticationException {
+
         String username = null;
-        String password = null;
 
-        // Check the Siteminder headers for authentication info
-        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);
-            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.
      *

+ 1 - 1
core/src/main/java/org/acegisecurity/ui/webapp/package.html

@@ -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>

+ 430 - 0
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) {
+        }
+    }
+}

+ 10 - 23
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);

+ 32 - 44
doc/docbook/acegi.xml

@@ -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>