From f1abf780b51415f77452b66faba1d53089bb0b6d Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Sun, 11 Apr 2004 12:09:08 +0000 Subject: [PATCH] Add support for HTTP Basic Authentication. --- changelog.txt | 2 + .../ui/basicauth/BasicProcessingFilter.java | 249 ++++++++++++ .../acegisecurity/ui/basicauth/package.html | 5 + .../acegisecurity/MockHttpServletRequest.java | 23 +- .../basicauth/BasicProcessingFilterTests.java | 376 ++++++++++++++++++ docs/reference/src/index.xml | 173 ++++++-- lib/jakarta-commons/commons-codec-LICENSE.txt | 56 +++ roadmap.txt | 2 - upgrade-04-05.txt | 6 +- 9 files changed, 858 insertions(+), 34 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java create mode 100644 core/src/main/java/org/acegisecurity/ui/basicauth/package.html create mode 100644 core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java create mode 100644 lib/jakarta-commons/commons-codec-LICENSE.txt diff --git a/changelog.txt b/changelog.txt index 9dbc9d387d..74d5f0b14e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,8 @@ Changes in version 0.5 (2004-xx-xx) ----------------------------------- +* Added support for HTTP Basic Authentication +* Added Burlap and Hessian remoting to Contacts sample application * AuthenticationProcessingFilter by default finds configuration context using Spring's WebApplicationContextUtils.getWebApplicationContext() * AuthenticationProcessingFilter context may optionally be specified with 'contextConfigLocation' param (was previously 'appContextLocation') * SecurityEnforcementFilter by default finds configuration context using Spring's WebApplicationContextUtils.getWebApplicationContext() diff --git a/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java new file mode 100644 index 0000000000..90c2d6d3d7 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java @@ -0,0 +1,249 @@ +/* Copyright 2004 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 net.sf.acegisecurity.ui.basicauth; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.AuthenticationManager; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.ui.webapp.HttpSessionIntegrationFilter; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import org.springframework.web.context.support.WebApplicationContextUtils; + +import java.io.IOException; + +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Processes a HTTP request's BASIC authorization headers, putting the result + * into the HttpSession. + * + *

+ * For a detailed background on what this filter is designed to process, refer + * to RFC 1945, Section + * 11.1. Any realm name presented in the HTTP request is ignored. + *

+ * + *

+ * In summary, this filter is responsible for processing any request that has a + * HTTP request header of Authorization with an authentication + * scheme of Basic and a Base64-encoded + * username:password token. For example, to authenticate user + * "Aladdin" with password "open sesame" the following header would be + * presented: + *

+ * + *

+ * Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==. + *

+ * + *

+ * Requests containing BASIC authentication headers are generally created by + * remoting protocol libraries. This filter is intended to process requests + * made by such libraries. + *

+ * + *

+ * If authentication is successful, the resulting {@link Authentication} object + * will be placed into the HttpSession with the attribute defined + * by {@link HttpSessionIntegrationFilter#ACEGI_SECURITY_AUTHENTICATION_KEY}. + *

+ * + *

+ * If authentication fails, a HttpServletResponse.SC_FORBIDDEN + * (403 error) response is sent. This is consistent with RFC 1945, Section 11, + * which states, "If the server does not wish to accept the credentials + * sent with a request, it should return a 403 (forbidden) response.". + *

+ * + *

+ * This filter works with an {@link AuthenticationManager} which is used to + * process each authentication request. By default, at init time, the filter + * will use Spring's {@link + * WebApplicationContextUtils#getWebApplicationContext(ServletContext sc)} + * method to obtain an ApplicationContext instance, inside which must be a + * configured AuthenticationManager instance. In the case where it is + * desirable for this filter to instantiate its own ApplicationContext + * instance from which to obtain the AuthenticationManager, the location of + * the config for this context may be specified with the optional + * contextConfigLocation init param. + *

+ * + *

+ * To use this filter, it is necessary to specify the following filter + * initialization parameters: + *

+ * + * + * + * + * @author Ben Alex + * @version $Id$ + */ +public class BasicProcessingFilter implements Filter { + //~ Static fields/initializers ============================================= + + /** + * Name of (optional) servlet filter parameter that can specify the config + * location for a new ApplicationContext used to config this filter. + */ + public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; + private static final Log logger = LogFactory.getLog(BasicProcessingFilter.class); + + //~ Instance fields ======================================================== + + private ApplicationContext ctx; + private AuthenticationManager authenticationManager; + private boolean ourContext = false; + + //~ Methods ================================================================ + + public void destroy() { + if (ourContext && ctx instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) ctx).close(); + } + } + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("Can only process HttpServletRequest"); + } + + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("Can only process HttpServletResponse"); + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String header = httpRequest.getHeader("Authorization"); + + if (logger.isDebugEnabled()) { + logger.debug("Authorization header: " + header); + } + + if ((header != null) && header.startsWith("Basic ")) { + String base64Token = header.substring(6); + String token = new String(Base64.decodeBase64( + base64Token.getBytes())); + + String username = ""; + String password = ""; + int delim = token.indexOf(":"); + + if (delim != -1) { + username = token.substring(0, delim); + password = token.substring(delim + 1); + } + + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, + password); + + Authentication authResult; + + try { + authResult = authenticationManager.authenticate(authRequest); + } catch (AuthenticationException failed) { + // Authentication failed + if (logger.isDebugEnabled()) { + logger.debug("Authentication request for user: " + username + + " failed: " + failed.toString()); + } + + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); // 403 + + return; + } + + // Authentication success + if (logger.isDebugEnabled()) { + logger.debug("Authentication success: " + authResult.toString()); + } + + httpRequest.getSession().setAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY, + authResult); + } + + chain.doFilter(request, response); + } + + public void init(FilterConfig filterConfig) throws ServletException { + String appContextLocation = filterConfig.getInitParameter(CONFIG_LOCATION_PARAM); + + if ((appContextLocation != null) && (appContextLocation.length() > 0)) { + ourContext = true; + + if (Thread.currentThread().getContextClassLoader().getResource(appContextLocation) == null) { + throw new ServletException("Cannot locate " + + appContextLocation); + } + } + + try { + if (!ourContext) { + ctx = WebApplicationContextUtils + .getRequiredWebApplicationContext(filterConfig + .getServletContext()); + } else { + ctx = new ClassPathXmlApplicationContext(appContextLocation); + } + } catch (RuntimeException e) { + throw new ServletException( + "Error obtaining/creating ApplicationContext for config. Must be stored in ServletContext, or optionally '" + + CONFIG_LOCATION_PARAM + + "' param may be used to allow creation of new context by this filter. See root error for additional details", + e); + } + + Map beans = ctx.getBeansOfType(AuthenticationManager.class, true, true); + + if (beans.size() == 0) { + throw new ServletException( + "Bean context must contain at least one bean of type AuthenticationManager"); + } + + String beanName = (String) beans.keySet().iterator().next(); + authenticationManager = (AuthenticationManager) beans.get(beanName); + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/basicauth/package.html b/core/src/main/java/org/acegisecurity/ui/basicauth/package.html new file mode 100644 index 0000000000..dcd7c31c91 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/basicauth/package.html @@ -0,0 +1,5 @@ + + +Authenticates HTTP BASIC authentication requests. + + diff --git a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java index c5a9acb684..b7a04e2ab6 100644 --- a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java +++ b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java @@ -48,7 +48,8 @@ public class MockHttpServletRequest implements HttpServletRequest { //~ Instance fields ======================================================== private HttpSession session; - private Map map = new HashMap(); + private Map paramMap = new HashMap(); + private Map headersMap = new HashMap(); private Principal principal; private String contextPath = ""; private String queryString = null; @@ -69,6 +70,12 @@ public class MockHttpServletRequest implements HttpServletRequest { super(); } + public MockHttpServletRequest(Map headers, Principal principal, HttpSession session) { + this.headersMap = headers; + this.principal = principal; + this.session = session; + } + //~ Methods ================================================================ public void setAttribute(String arg0, Object arg1) { @@ -120,7 +127,13 @@ public class MockHttpServletRequest implements HttpServletRequest { } public String getHeader(String arg0) { - throw new UnsupportedOperationException("mock method not implemented"); + Object result = headersMap.get(arg0); + + if (result != null) { + return (String) headersMap.get(arg0); + } else { + return null; + } } public Enumeration getHeaderNames() { @@ -152,14 +165,14 @@ public class MockHttpServletRequest implements HttpServletRequest { } public void setParameter(String arg0, String value) { - map.put(arg0, value); + paramMap.put(arg0, value); } public String getParameter(String arg0) { - Object result = map.get(arg0); + Object result = paramMap.get(arg0); if (result != null) { - return (String) map.get(arg0); + return (String) paramMap.get(arg0); } else { return null; } diff --git a/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java new file mode 100644 index 0000000000..e3f0e6b530 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java @@ -0,0 +1,376 @@ +/* Copyright 2004 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 net.sf.acegisecurity.ui.basicauth; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.MockFilterConfig; +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; +import net.sf.acegisecurity.MockHttpSession; +import net.sf.acegisecurity.ui.webapp.HttpSessionIntegrationFilter; + +import org.apache.commons.codec.binary.Base64; + +import java.io.IOException; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + + +/** + * Tests {@link BasicProcessingFilter}. + * + * @author Ben Alex + * @version $Id$ + */ +public class BasicProcessingFilterTests extends TestCase { + //~ Constructors =========================================================== + + public BasicProcessingFilterTests() { + super(); + } + + public BasicProcessingFilterTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(BasicProcessingFilterTests.class); + } + + public void testDoFilterWithNonHttpServletRequestDetected() + throws Exception { + BasicProcessingFilter filter = new BasicProcessingFilter(); + + try { + filter.doFilter(null, new MockHttpServletResponse(), + new MockFilterChain()); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertEquals("Can only process HttpServletRequest", + expected.getMessage()); + } + } + + public void testDoFilterWithNonHttpServletResponseDetected() + throws Exception { + BasicProcessingFilter filter = new BasicProcessingFilter(); + + try { + filter.doFilter(new MockHttpServletRequest(null, null), null, + new MockFilterChain()); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertEquals("Can only process HttpServletResponse", + expected.getMessage()); + } + } + + public void testFilterIgnoresRequestsContainingNoAuthorizationHeader() + throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will be invoked + MockFilterChain chain = new MockFilterChain(true); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); + } + + public void testInvalidBasicAuthorizationTokenIsIgnored() + throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; + headers.put("Authorization", + "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will be invoked + MockFilterChain chain = new MockFilterChain(true); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); + } + + public void testNormalOperation() throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + String token = "marissa:koala"; + headers.put("Authorization", + "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will be invoked + MockFilterChain chain = new MockFilterChain(true); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) != null); + assertEquals("marissa", + ((Authentication) request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY)).getPrincipal() + .toString()); + } + + public void testOtherAuthorizationSchemeIsIgnored() + throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + headers.put("Authorization", "SOME_OTHER_AUTHENTICATION_SCHEME"); + + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will be invoked + MockFilterChain chain = new MockFilterChain(true); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); + } + + public void testStartupDetectsInvalidContextConfigLocation() + throws Exception { + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-invalid.xml"); + + BasicProcessingFilter filter = new BasicProcessingFilter(); + + try { + filter.init(config); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertEquals("Bean context must contain at least one bean of type AuthenticationManager", + expected.getMessage()); + } + } + + public void testStartupDetectsMissingAppContext() throws Exception { + MockFilterConfig config = new MockFilterConfig(); + + BasicProcessingFilter filter = new BasicProcessingFilter(); + + try { + filter.init(config); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertTrue(expected.getMessage().startsWith("Error obtaining/creating ApplicationContext for config.")); + } + + config.setInitParmeter("contextConfigLocation", ""); + + try { + filter.init(config); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertTrue(expected.getMessage().startsWith("Error obtaining/creating ApplicationContext for config.")); + } + } + + public void testStartupDetectsMissingInvalidContextConfigLocation() + throws Exception { + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", "DOES_NOT_EXIST"); + + BasicProcessingFilter filter = new BasicProcessingFilter(); + + try { + filter.init(config); + fail("Should have thrown ServletException"); + } catch (ServletException expected) { + assertTrue(expected.getMessage().startsWith("Cannot locate")); + } + } + + public void testSuccessLoginThenFailureLoginResultsInSessionLoosingToken() + throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + String token = "marissa:koala"; + headers.put("Authorization", + "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will be invoked + MockFilterChain chain = new MockFilterChain(true); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) != null); + assertEquals("marissa", + ((Authentication) request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY)).getPrincipal() + .toString()); + + // NOW PERFORM FAILED AUTHENTICATION + // Setup our HTTP request + headers = new HashMap(); + token = "marissa:WRONG_PASSWORD"; + headers.put("Authorization", + "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + request = new MockHttpServletRequest(headers, null, + new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our expectation that the filter chain will not be invoked, as we get a 403 forbidden response + chain = new MockFilterChain(false); + response = new MockHttpServletResponse(); + + // Test + filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); + assertEquals(403, response.getError()); + } + + public void testWrongPasswordReturnsForbidden() throws Exception { + // Setup our HTTP request + Map headers = new HashMap(); + String token = "marissa:WRONG_PASSWORD"; + headers.put("Authorization", + "Basic " + new String(Base64.encodeBase64(token.getBytes()))); + + MockHttpServletRequest request = new MockHttpServletRequest(headers, + null, new MockHttpSession()); + request.setServletPath("/some_file.html"); + + // Setup our filter configuration + MockFilterConfig config = new MockFilterConfig(); + config.setInitParmeter("contextConfigLocation", + "net/sf/acegisecurity/ui/webapp/filtertest-valid.xml"); + + // Setup our expectation that the filter chain will not be invoked, as we get a 403 forbidden response + MockFilterChain chain = new MockFilterChain(false); + MockHttpServletResponse response = new MockHttpServletResponse(); + + // Test + BasicProcessingFilter filter = new BasicProcessingFilter(); + executeFilterInContainerSimulator(config, filter, request, response, + chain); + + assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); + assertEquals(403, response.getError()); + } + + private void executeFilterInContainerSimulator(FilterConfig filterConfig, + Filter filter, ServletRequest request, ServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + filter.init(filterConfig); + filter.doFilter(request, response, filterChain); + filter.destroy(); + } + + //~ Inner Classes ========================================================== + + private class MockFilterChain implements FilterChain { + private boolean expectToProceed; + + public MockFilterChain(boolean expectToProceed) { + this.expectToProceed = expectToProceed; + } + + private MockFilterChain() { + super(); + } + + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + if (expectToProceed) { + assertTrue(true); + } else { + fail("Did not expect filter chain to proceed"); + } + } + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 0650a3abf9..c877ee551f 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -281,15 +281,6 @@ custom request contexts implement the SecureContext interface. - - - Future Work - - Over time it is hoped that the Spring remoting classes can be - extended to support propagation of the Context - between ContextHolders on the client and - server. - @@ -618,7 +609,7 @@ expressions to be treated as Apache Ant paths. It is not possible to mix expression syntaxes within the same definition. For example, the earlier configuration could be generated using Apache Ant paths as - follows: + follows: <bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> @@ -642,8 +633,8 @@ pattern appears higher than the less specific /super/ pattern. If they were reversed, the /super/ pattern would always match and the - /secure/super/ pattern would never be evaluated. - + /secure/super/ pattern would never be + evaluated. The special keyword CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON causes @@ -1477,13 +1468,17 @@ public boolean supports(Class clazz); - Several alternatives are available for the first step. The two - most supported approaches are HTTP Session Authentication, which uses - the HttpSession object and filters to authenticate - the user. The other is via Container Adapters, which allow supported - web containers to perform the authentication themselves. HTTP Session - Authentication is discussed below, whilst Container Adapters are - discussed in a separate section. + There are several alternatives are available for the first step, + which will be briefly discussed in this chapter. The most popular + approach is HTTP Session Authentication, which uses the + HttpSession object and filters to authenticate the + user. Another approach is HTTP Basic Authentication, which allows + clients to use HTTP headers to present authentication information to + the Acegi Security System for Spring. The final approach is via + Container Adapters, which allow supported web containers to perform + the authentication themselves. HTTP Session Authentication is + discussed below, whilst Container Adapters are discussed in a separate + section. @@ -1569,11 +1564,74 @@ public boolean supports(Class clazz); be used instead of Container Adapters. + + HTTP Basic Authentication + + Primarily to cater for the needs of remoting protocols such as + Hessian and Burlap, the Acegi Security System for Spring provides a + BasicProcessingFilter which is capable of + processing authentication credentials presented in HTTP headers (for + standard authentication of web browser users, we recommend HTTP + Session Authentication). The standard governing HTTP Basic + Authentication is defined by RFC 1945, Section 11, and the + BasicProcessingFilter conforms with this RFC. To + implement HTTP Basic Authentication, it is necessary to add the + following filter to web.xml: + + <filter> + <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name> + <filter-class>net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter</filter-class> +</filter> + +<filter-mapping> + <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name> + <url-pattern>/*</url-pattern> +</filter-mapping> + + Like the AuthenticationProcessingFilter + discussed above, the BasicProcessingFilter will + need to delegate to a properly configured + AuthenticationManager. To do this it requires + access to a Spring application context, which is usually obtained from + WebApplicationContextUtils.getWebApplicationContext(ServletContext). + This is usually made available by using Spring's + ContextLoaderListener in + web.xml. Alternatively, the + web.xml can be used to define a filter + <init-param> named + contextConfigLocation. This initialization + parameter will represent a path to a Spring XML application context + that the AuthenticationProcessingFilter will load + during startup. + + The AuthenticationManager processes each + authentication request. If authentication fails, a 403 (forbidden) + response will be returned in response to the HTTP request. If + authentication is successful, the resulting + Authentication object will be placed into the + HttpSession attribute indicated by + HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY. + This becomes the "well-known location" from which the + Authentication object is later extracted. + + If the authentication event was successful, or authentication + was not attempted because the HTTP header did not contain a supported + authentication request, the filter chain will continue as normal. The + only time the filter chain will be interrupted is if authentication + fails and a 403 response is returned, as discussed in the previous + paragraph. + + HTTP Basic Authentication is recommended to be used instead of + Container Adapters. It can be used in conjunction with HTTP Session + Authentication, as demonstrated in the Contacts sample + application. + + Well-Known Location Integration Once a web application has used either HTTP Session - Authentication or a Container Adapter, an + Authentication, HTTP Basic Authentication, or a Container Adapter, an Authentication object will exist in a well-known location. The final step in automatically integrating the user interface with the backend security interceptor is to extract this @@ -1597,9 +1655,10 @@ public boolean supports(Class clazz); HttpSessionIntegrationFilter is used - with HTTP Session Authentication, or any other approach that - populates the HttpSession accordingly. It - extracts the Authentication object from the + with HTTP Session Authentication, HTTP Basic Authentication, or + any other approach that populates the + HttpSession accordingly. It extracts the + Authentication object from the HttpSession attribute indicated by HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY. @@ -1663,8 +1722,8 @@ public boolean supports(Class clazz); with end users. Whilst this worked well, it required considerable time to support multiple container versions and the configuration itself was relatively time-consuming for developers. For this reason the HTTP - Session Authentication approach was developed, and is today - recommended for most applications. + Session Authentication and HTTP Basic Authentication approaches were + developed, and are today recommended for most applications. Container Adapters enable the Acegi Security System for Spring to integrate directly with the containers used to host end user @@ -1789,6 +1848,18 @@ public boolean supports(Class clazz); acegi-security-catalina-common.jar + + + commons-codec.jar + + + + burlap.jar + + + + hessian.jar + None of the above JAR files (or @@ -1851,6 +1922,18 @@ $CATALINA_HOME/bin/startup.sh acegi-security-jetty-ext.jar + + + commons-codec.jar + + + + burlap.jar + + + + hessian.jar + None of the above JAR files (or @@ -1904,6 +1987,18 @@ $CATALINA_HOME/bin/startup.sh acegi-security-jboss-lib.jar + + + commons-codec.jar + + + + burlap.jar + + + + hessian.jar + None of the above JAR files (or @@ -1954,6 +2049,18 @@ $CATALINA_HOME/bin/startup.sh acegi-security-resin-lib.jar + + + commons-codec.jar + + + + burlap.jar + + + + hessian.jar + Unlike the container-wide acegisecurity.xml @@ -2066,6 +2173,22 @@ $CATALINA_HOME/bin/startup.sh visiting http://localhost:8080/contacts/secure/super, which will demonstrate access being denied by the SecurityEnforcementFilter. + + The Contacts sample application also include a + client directory. Inside you will find a small + application that queries the backend business objects using the Hessian + and Burlap protocols. This demonstrates how to use the Acegi Security + System for Spring for authentication with Spring remoting protocols. To + try this client, ensure your servlet container is still running the + Contacts sample application, and then execute client + marissa. This will use the remoting protocols to obtain the + list of contacts with the owner specified (in this case + marissa). Note you will be need to edit + client.properties to use a different username, + password, or target URL. To see that security does indeed work, try + running client scott before changing + client.properties to use scott's + authentication details. diff --git a/lib/jakarta-commons/commons-codec-LICENSE.txt b/lib/jakarta-commons/commons-codec-LICENSE.txt new file mode 100644 index 0000000000..2cddf5ef60 --- /dev/null +++ b/lib/jakarta-commons/commons-codec-LICENSE.txt @@ -0,0 +1,56 @@ +/* + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2001-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgement may appear in the software itself, + * if and wherever such third-party acknowledgements normally appear. + * + * 4. The names "Apache", "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache" nor may "Apache" appear in their name without prior + * written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ diff --git a/roadmap.txt b/roadmap.txt index 79aee91ad0..8da4ff1456 100644 --- a/roadmap.txt +++ b/roadmap.txt @@ -9,8 +9,6 @@ to the project have some ideas on where they could potentially add value. - Extend integration tests to support Resin (Ant startup/shutdown approach needed) -- Extend Spring remoting classes to transparently transport the Context - - Sample application that demonstrates EJB remote method invocation with Acegi security system as login module on server side diff --git a/upgrade-04-05.txt b/upgrade-04-05.txt index 2a8c240054..f69b792fd0 100644 --- a/upgrade-04-05.txt +++ b/upgrade-04-05.txt @@ -2,8 +2,7 @@ ACEGI SECURITY SYSTEM FOR SPRING - UPGRADING FROM 0.4 TO 0.5 =============================================================================== -Unfortunately, changes to the API and package locations were required. The -following should help most casual users of the project update their +The following should help most casual users of the project update their applications: - By default, AuthenticationProcessingFilter and SecurityEnforcementFilter now @@ -17,6 +16,9 @@ applications: If you do still want to use this approach, just rename your param from 'appContextLocation' to 'contextConfigLocation'. +- If you're using container adapters, please refer to the reference + documentation as additional JARs are now required in your container + classloader. We hope you find the new features useful in your projects.