From 4717b64b83cfdad5f407aa6097227369f9813b50 Mon Sep 17 00:00:00 2001 From: Scott McCrory Date: Sun, 25 Sep 2005 22:48:33 +0000 Subject: [PATCH] Updated Siteminder auth processing filter and added test case. As of this weekend, this version is in production at a large financial org. --- ...eminderAuthenticationProcessingFilter.java | 175 ++++++++++-------- ...erAuthenticationProcessingFilterTests.java | 164 ++++++++++++++++ 2 files changed, 260 insertions(+), 79 deletions(-) create mode 100644 core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java 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 f5f1a0c8ed..caa81e11d4 100644 --- a/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java +++ b/core/src/main/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilter.java @@ -10,48 +10,43 @@ import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; import net.sf.acegisecurity.ui.WebAuthenticationDetails; /** - * Extends Acegi's AuthenticationProcessingFilter to use Siteminder's headers for identification. + * Extends Acegi's AuthenticationProcessingFilter to pick up Netegrity + * Siteminder's headers. * *

- * Provides the ability set all source key names and also provides form-based authentication as a backup. - *

- * - *

- * You must set the Siteminder header keys, otherwise Siteminder checks will be skipped (there - * are no defaults). If the Siteminder check is unsuccessful or if the headers are not - * defined/found in the HTTP request, then the form parameters will be checked (see below). - * This allows applications to function even when their Siteminder infrastructure - * is unavailable, as is often the case during development. If you do not wish to use backup - * form-based authentication, then set the form parameter keys to null/blank. + * Also provides a backup form-based authentication and the ability set source + * key names. *

* *

- * Siteminder must present at least one HTTP header to this filter - typically - * containing a unique identifier such a username, an employee number or a national ID. - * This makes sense because Siteminder has already authenticated the user prior to - * getting to this filter, so we're really only using it for identification and not authentication. - * Set the siteminderUsernameHeaderKey value to tell the filter where to greb the "username" - * value. You'll typically also set the siteminderPasswordHeaderKey to the same header key. - * Just remember to modify your AuthenticationDAO so that it can handle identity-only requests! + * 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. *

* *

- * Forms must present two parameters to this filter: a username and a password. - * If not specified, the parameter names to use are defaulted to the static fields - * {@link #ACEGI_SECURITY_FORM_USERNAME_KEY} and {@link #ACEGI_SECURITY_FORM_PASSWORD_KEY}. + * 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 net.sf.acegisecurity.util.FilterChainProxy} and include this filter. + * Do not use this class directly. Instead, configure + * web.xml to use the + * {@link net.sf.acegisecurity.util.FilterToBeanProxy}. *

* * @author Scott McCrory - * @author Ben Alex - * @since 0.9.0 - * @version CVS $Id$ + * @version CVS $Id: SiteminderAuthenticationProcessingFilter.java,v 1.6 + * 2005/08/19 14:31:30 E099544 Exp $ */ -public class SiteminderAuthenticationProcessingFilter extends AuthenticationProcessingFilter { +public class SiteminderAuthenticationProcessingFilter extends + AuthenticationProcessingFilter { /** * Siteminder username header key. @@ -80,106 +75,128 @@ public class SiteminderAuthenticationProcessingFilter extends AuthenticationProc super(); } - /*** - * This filter by default responds to /j_acegi_security_check. - * - * @return the default - */ + /*************************************************************************** + * This filter by default responds to /j_acegi_security_check. + * + * @return the default + */ public String getDefaultFilterProcessesUrl() { return "/j_acegi_security_check"; } - public Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException { + /** + * @see net.sf.acegisecurity.ui.AbstractProcessingFilter#attemptAuthentication(javax.servlet.http.HttpServletRequest) + */ + public Authentication attemptAuthentication(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) { + && siteminderUsernameHeaderKey.length() > 0 + && siteminderPasswordHeaderKey != null + && siteminderPasswordHeaderKey.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 authentication info wasn't available, then get it + // from the form parameters + if (username == null || username.length() == 0 || password == null + || password.length() == 0) { - //System.out.println("Siteminder headers not found for authentication"); + System.out + .println("Siteminder headers not found for authentication, so trying to use form values"); - if (formUsernameParameterKey != null && formUsernameParameterKey.length() > 0) { + if (formUsernameParameterKey != null + && formUsernameParameterKey.length() > 0) { username = request.getParameter(formUsernameParameterKey); - } - else { - username = request.getParameter(ACEGI_SECURITY_FORM_USERNAME_KEY); + } else { + username = request + .getParameter(ACEGI_SECURITY_FORM_USERNAME_KEY); } password = obtainPassword(request); } - // If either are null, set them to blank to avoid a NPE. - if (username == null) { + // Convert username and password to upper case. This is normally not a + // good practice but we do it here because Siteminder gives us the username + // in lower case, while most backing systems store it in upper case. + if (username != null) { + username = username.toUpperCase(); + } else { + // If username is null, set to blank to avoid a NPE. username = ""; } - if (password == null) { + 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); + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( + username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); // Place the last username attempted into HttpSession for views - request.getSession().setAttribute(ACEGI_SECURITY_LAST_USERNAME_KEY, username); + request.getSession().setAttribute(ACEGI_SECURITY_LAST_USERNAME_KEY, + username); return this.getAuthenticationManager().authenticate(authRequest); + } + /** + * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) + */ public void init(FilterConfig filterConfig) throws ServletException { } - /*** - * Provided so that subclasses may configure what is put into the - * authentication request's details property. The default implementation - * simply constructs {@link WebAuthenticationDetails}. - * - * @param request that an authentication request is being created for - * @param authRequest the authentication request object that should have - * its details set - */ - protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { + /*************************************************************************** + * Provided so that subclasses may configure what is put into the + * authentication request's details property. The default implementation + * simply constructs {@link WebAuthenticationDetails}. + * + * @param request that an authentication request is being created for + * @param authRequest the authentication request object that should have its details set + */ + protected void setDetails(HttpServletRequest request, + UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(new WebAuthenticationDetails(request)); } - /*** - * Enables subclasses to override the composition of the password, such as - * by including additional values and a separator. - * - *

- * This might be used for example if a postcode/zipcode was required in - * addition to the password. A delimiter such as a pipe (|) should be used - * to separate the password and extended value(s). The - * AuthenticationDao will need to generate the expected - * password in a corresponding manner. - *

- * - * @param request so that request attributes can be retrieved - * - * @return the password that will be presented in the - * Authentication request token to the - * AuthenticationManager - */ + /*************************************************************************** + * Enables subclasses to override the composition of the password, such as + * by including additional values and a separator. + * + *

+ * This might be used for example if a postcode/zipcode was required in + * addition to the password. A delimiter such as a pipe (|) should be used + * to separate the password and extended value(s). The + * AuthenticationDao will need to generate the expected + * password in a corresponding manner. + *

+ * + * @param request so that request attributes can be retrieved + * + * @return the password that will be presented in the + * Authentication request token to the + * AuthenticationManager + */ protected String obtainPassword(HttpServletRequest request) { - if (formPasswordParameterKey != null && formPasswordParameterKey.length() > 0) { + if (formPasswordParameterKey != null + && formPasswordParameterKey.length() > 0) { return request.getParameter(formPasswordParameterKey); - } - else { + } else { return request.getParameter(ACEGI_SECURITY_FORM_PASSWORD_KEY); } diff --git a/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java new file mode 100644 index 0000000000..aff5e0e7a4 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/webapp/SiteminderAuthenticationProcessingFilterTests.java @@ -0,0 +1,164 @@ +package net.sf.acegisecurity.ui.webapp; + +import junit.framework.TestCase; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.MockAuthenticationManager; +import net.sf.acegisecurity.ui.WebAuthenticationDetails; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Tests SiteminderAuthenticationProcessingFilter. + * + * @author Ben Alex + * @author Scott McCrory + * @version CVS $Id$ + */ +public class SiteminderAuthenticationProcessingFilterTests extends TestCase { + + /** + * Basic constructor. + */ + public SiteminderAuthenticationProcessingFilterTests() { + super(); + } + + /** + * Argument constructor. + * + * @param arg0 + */ + public SiteminderAuthenticationProcessingFilterTests(String arg0) { + super(arg0); + } + + /** + * @see junit.framework.TestCase#setUp() + */ + public final void setUp() throws Exception { + super.setUp(); + } + + /** + * Runs the tests as a command-line program. + * + * @param args + */ + public static void main(String[] args) { + junit.textui.TestRunner + .run(SiteminderAuthenticationProcessingFilterTests.class); + } + + /** + * Tests the class' getters. + */ + public void testGetters() { + SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); + assertEquals("/j_acegi_security_check", filter + .getDefaultFilterProcessesUrl()); + } + + /** + * Tests normal form processing. + * + * @throws Exception + */ + public void testFormNormalOperation() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request + .addParameter( + SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY, + "marissa"); + request + .addParameter( + SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_PASSWORD_KEY, + "koala"); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(true); + + SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.init(null); + + Authentication result = filter.attemptAuthentication(request); + assertTrue(result != null); + assertEquals("127.0.0.1", ((WebAuthenticationDetails) result + .getDetails()).getRemoteAddress()); + + } + + /** + * Tests form null password handling. + * + * @throws Exception + */ + public void testFormNullPasswordHandledGracefully() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request + .addParameter( + SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY, + "marissa"); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(true); + + SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.init(null); + + Authentication result = filter.attemptAuthentication(request); + assertTrue(result != null); + + } + + /** + * Tests form null username handling. + * + * @throws Exception + */ + public void testFormNullUsernameHandledGracefully() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request + .addParameter( + SiteminderAuthenticationProcessingFilter.ACEGI_SECURITY_FORM_PASSWORD_KEY, + "koala"); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(true); + + SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.init(null); + + Authentication result = filter.attemptAuthentication(request); + assertTrue(result != null); + + } + + /** + * Tests normal Siteminder header processing. + * + * @throws Exception + */ + public void testSiteminderNormalOperation() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("SM_USER", "E099544"); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(true); + + SiteminderAuthenticationProcessingFilter filter = new SiteminderAuthenticationProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.setSiteminderUsernameHeaderKey("SM_USER"); + filter.setSiteminderPasswordHeaderKey("SM_USER"); + filter.init(null); + + Authentication result = filter.attemptAuthentication(request); + assertTrue(result != null); + assertEquals("127.0.0.1", ((WebAuthenticationDetails) result + .getDetails()).getRemoteAddress()); + + } + +}