diff --git a/core/src/main/java/org/acegisecurity/intercept/web/AuthenticationEntryPoint.java b/core/src/main/java/org/acegisecurity/intercept/web/AuthenticationEntryPoint.java new file mode 100644 index 0000000000..91532de055 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/intercept/web/AuthenticationEntryPoint.java @@ -0,0 +1,56 @@ +/* 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.intercept.web; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + + +/** + * Used by {@link SecurityEnforcementFilter} to commence an authentication + * scheme. + * + * @author Ben Alex + * @version $Id$ + */ +public interface AuthenticationEntryPoint { + //~ Methods ================================================================ + + /** + * Commences an authentication scheme. + * + *

+ * SecurityEnforcementFilter will populate the + * HttpSession attribute named + * AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY + * with the requested target URL before calling this method. + *

+ * + *

+ * Implementations should modify the headers on the + * ServletResponse to as necessary to commence the + * authentication process. + *

+ * + * @param request that resulted in an AuthenticationException + * @param response so that the user agent can begin authentication + */ + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException; +} diff --git a/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java b/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java index 32f816826f..712f269b3a 100644 --- a/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java +++ b/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java @@ -59,8 +59,7 @@ import javax.servlet.http.HttpServletResponse; *

* *

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

* * * @@ -92,16 +92,20 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean { //~ Instance fields ======================================================== - protected FilterSecurityInterceptor filterSecurityInterceptor; - - /** - * The URL that should be used for redirection if an - * AuthenticationException is detected. - */ - protected String loginFormUrl; + private AuthenticationEntryPoint authenticationEntryPoint; + private FilterSecurityInterceptor filterSecurityInterceptor; //~ Methods ================================================================ + public void setAuthenticationEntryPoint( + AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + } + + public AuthenticationEntryPoint getAuthenticationEntryPoint() { + return authenticationEntryPoint; + } + public void setFilterSecurityInterceptor( FilterSecurityInterceptor filterSecurityInterceptor) { this.filterSecurityInterceptor = filterSecurityInterceptor; @@ -111,17 +115,10 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean { return filterSecurityInterceptor; } - public void setLoginFormUrl(String loginFormUrl) { - this.loginFormUrl = loginFormUrl; - } - - public String getLoginFormUrl() { - return loginFormUrl; - } - public void afterPropertiesSet() throws Exception { - if ((loginFormUrl == null) || "".equals(loginFormUrl)) { - throw new IllegalArgumentException("loginFormUrl must be specified"); + if (authenticationEntryPoint == null) { + throw new IllegalArgumentException( + "authenticationEntryPoint must be specified"); } if (filterSecurityInterceptor == null) { @@ -161,8 +158,7 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean { ((HttpServletRequest) request).getSession().setAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY, fi.getRequestUrl()); - ((HttpServletResponse) response).sendRedirect(((HttpServletRequest) request) - .getContextPath() + loginFormUrl); + authenticationEntryPoint.commence(request, response); } catch (AccessDeniedException accessDenied) { if (logger.isDebugEnabled()) { logger.debug( diff --git a/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java index 47b0c902f6..1107970f9a 100644 --- a/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java +++ b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java @@ -18,6 +18,7 @@ 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.intercept.web.AuthenticationEntryPoint; import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; import net.sf.acegisecurity.ui.webapp.HttpSessionIntegrationFilter; @@ -63,9 +64,9 @@ import javax.servlet.http.HttpServletResponse; *

* *

- * Requests containing BASIC authentication headers are generally created by - * remoting protocol libraries. This filter is intended to process requests - * made by such libraries. + * This filter can be used to provide BASIC authentication services to both + * remoting protocol clients (such as Hessian and SOAP) as well as standard + * user agents (such as Internet Explorer and Netscape). *

* *

@@ -75,10 +76,9 @@ import javax.servlet.http.HttpServletResponse; *

* *

- * 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.". + * If authentication fails, an {@link AuthenticationEntryPoint} implementation + * is called. Usually this should be {@link BasicProcessingFilterEntryPoint}, + * which will prompt the user to authenticate again via BASIC authentication. *

* *

@@ -97,10 +97,20 @@ public class BasicProcessingFilter implements Filter, InitializingBean { //~ Instance fields ======================================================== + private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; //~ Methods ================================================================ + public void setAuthenticationEntryPoint( + AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + } + + public AuthenticationEntryPoint getAuthenticationEntryPoint() { + return authenticationEntryPoint; + } + public void setAuthenticationManager( AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; @@ -115,6 +125,11 @@ public class BasicProcessingFilter implements Filter, InitializingBean { throw new IllegalArgumentException( "An AuthenticationManager is required"); } + + if (this.authenticationEntryPoint == null) { + throw new IllegalArgumentException( + "An AuthenticationEntryPoint is required"); + } } public void destroy() {} @@ -166,7 +181,7 @@ public class BasicProcessingFilter implements Filter, InitializingBean { + " failed: " + failed.toString()); } - ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); // 403 + authenticationEntryPoint.commence(request, response); return; } diff --git a/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPoint.java b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPoint.java new file mode 100644 index 0000000000..00bda24770 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPoint.java @@ -0,0 +1,75 @@ +/* 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.intercept.web.AuthenticationEntryPoint; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + + +/** + * Used by the SecurityEnforcementFilter to commence + * authentication via the {@link BasicProcessingFilter}. + * + *

+ * Once a user agent is authenticated using BASIC authentication, logout + * requires that the browser be closed or an unauthorized (401) header be + * sent. The simplest way of achieving the latter is to call the {@link + * #commence(ServletRequest, ServletResponse)} method below. This will + * indicate to the browser its credentials are no longer authorized, causing + * it to prompt the user to login again. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class BasicProcessingFilterEntryPoint implements AuthenticationEntryPoint, + InitializingBean { + //~ Instance fields ======================================================== + + private String realmName; + + //~ Methods ================================================================ + + public void setRealmName(String realmName) { + this.realmName = realmName; + } + + public String getRealmName() { + return realmName; + } + + public void afterPropertiesSet() throws Exception { + if ((realmName == null) || "".equals(realmName)) { + throw new IllegalArgumentException("realmName must be specified"); + } + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.addHeader("WWW-Authenticate", + "Basic realm=\"" + realmName + "\""); + httpResponse.sendError(401); + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPoint.java b/core/src/main/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPoint.java new file mode 100644 index 0000000000..c42a3cdd43 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPoint.java @@ -0,0 +1,69 @@ +/* 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.webapp; + +import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Used by the SecurityEnforcementFilter to commence + * authentication via the {@link AuthenticationProcessingFilter}. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationProcessingFilterEntryPoint + implements AuthenticationEntryPoint, InitializingBean { + //~ Instance fields ======================================================== + + /** + * The URL where the AuthenticationProcessingFilter login page + * can be found. + */ + private String loginFormUrl; + + //~ Methods ================================================================ + + public void setLoginFormUrl(String loginFormUrl) { + this.loginFormUrl = loginFormUrl; + } + + public String getLoginFormUrl() { + return loginFormUrl; + } + + public void afterPropertiesSet() throws Exception { + if ((loginFormUrl == null) || "".equals(loginFormUrl)) { + throw new IllegalArgumentException("loginFormUrl must be specified"); + } + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + ((HttpServletResponse) response).sendRedirect(((HttpServletRequest) request) + .getContextPath() + loginFormUrl); + } +} diff --git a/core/src/test/java/org/acegisecurity/MockAuthenticationEntryPoint.java b/core/src/test/java/org/acegisecurity/MockAuthenticationEntryPoint.java new file mode 100644 index 0000000000..8a286d0653 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/MockAuthenticationEntryPoint.java @@ -0,0 +1,57 @@ +/* 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; + +import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Performs a HTTP redirect to the constructor-indicated URL. + * + * @author Ben Alex + * @version $Id$ + */ +public class MockAuthenticationEntryPoint implements AuthenticationEntryPoint { + //~ Instance fields ======================================================== + + private String url; + + //~ Constructors =========================================================== + + public MockAuthenticationEntryPoint(String url) { + this.url = url; + } + + private MockAuthenticationEntryPoint() { + super(); + } + + //~ Methods ================================================================ + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + ((HttpServletResponse) response).sendRedirect(((HttpServletRequest) request) + .getContextPath() + url); + } +} diff --git a/core/src/test/java/org/acegisecurity/MockHttpServletResponse.java b/core/src/test/java/org/acegisecurity/MockHttpServletResponse.java index ad6010fc51..61c0db7f82 100644 --- a/core/src/test/java/org/acegisecurity/MockHttpServletResponse.java +++ b/core/src/test/java/org/acegisecurity/MockHttpServletResponse.java @@ -18,7 +18,9 @@ package net.sf.acegisecurity; import java.io.IOException; import java.io.PrintWriter; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; @@ -35,6 +37,7 @@ import javax.servlet.http.HttpServletResponse; public class MockHttpServletResponse implements HttpServletResponse { //~ Instance fields ======================================================== + private Map headersMap = new HashMap(); private String redirect; private int error; @@ -76,6 +79,16 @@ public class MockHttpServletResponse implements HttpServletResponse { throw new UnsupportedOperationException("mock method not implemented"); } + public String getHeader(String arg0) { + Object result = headersMap.get(arg0); + + if (result != null) { + return (String) headersMap.get(arg0); + } else { + return null; + } + } + public void setIntHeader(String arg0, int arg1) { throw new UnsupportedOperationException("mock method not implemented"); } @@ -117,7 +130,7 @@ public class MockHttpServletResponse implements HttpServletResponse { } public void addHeader(String arg0, String arg1) { - throw new UnsupportedOperationException("mock method not implemented"); + headersMap.put(arg0, arg1); } public void addIntHeader(String arg0, int arg1) { diff --git a/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java b/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java index 8427ac4d26..51eed372f4 100644 --- a/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java +++ b/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java @@ -19,6 +19,7 @@ import junit.framework.TestCase; import net.sf.acegisecurity.AccessDeniedException; import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.MockAuthenticationEntryPoint; import net.sf.acegisecurity.MockHttpServletRequest; import net.sf.acegisecurity.MockHttpServletResponse; import net.sf.acegisecurity.MockHttpSession; @@ -76,7 +77,8 @@ public class SecurityEnforcementFilterTests extends TestCase { // Test SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); filter.setFilterSecurityInterceptor(interceptor); - filter.setLoginFormUrl("/login.jsp"); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "/login.jsp")); MockHttpServletResponse response = new MockHttpServletResponse(); filter.doFilter(request, response, chain); @@ -115,8 +117,9 @@ public class SecurityEnforcementFilterTests extends TestCase { false, false)); assertTrue(filter.getFilterSecurityInterceptor() != null); - filter.setLoginFormUrl("/u"); - assertEquals("/u", filter.getLoginFormUrl()); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "/login.jsp")); + assertTrue(filter.getAuthenticationEntryPoint() != null); } public void testRedirectedToLoginFormAndSessionShowsOriginalTargetWhenAuthenticationException() @@ -136,7 +139,8 @@ public class SecurityEnforcementFilterTests extends TestCase { // Test SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); filter.setFilterSecurityInterceptor(interceptor); - filter.setLoginFormUrl("/login.jsp"); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "/login.jsp")); filter.afterPropertiesSet(); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -146,21 +150,7 @@ public class SecurityEnforcementFilterTests extends TestCase { request.getSession().getAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY)); } - public void testStartupDetectsMissingFilterSecurityInterceptor() - throws Exception { - SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); - filter.setLoginFormUrl("/login.jsp"); - - try { - filter.afterPropertiesSet(); - fail("Should have thrown IllegalArgumentException"); - } catch (IllegalArgumentException expected) { - assertEquals("filterSecurityInterceptor must be specified", - expected.getMessage()); - } - } - - public void testStartupDetectsMissingLoginFormUrl() + public void testStartupDetectsMissingAuthenticationEntryPoint() throws Exception { SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); filter.setFilterSecurityInterceptor(new MockFilterSecurityInterceptor( @@ -170,7 +160,23 @@ public class SecurityEnforcementFilterTests extends TestCase { filter.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { - assertEquals("loginFormUrl must be specified", expected.getMessage()); + assertEquals("authenticationEntryPoint must be specified", + expected.getMessage()); + } + } + + public void testStartupDetectsMissingFilterSecurityInterceptor() + throws Exception { + SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "/login.jsp")); + + try { + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("filterSecurityInterceptor must be specified", + expected.getMessage()); } } @@ -190,7 +196,8 @@ public class SecurityEnforcementFilterTests extends TestCase { // Test SecurityEnforcementFilter filter = new SecurityEnforcementFilter(); filter.setFilterSecurityInterceptor(interceptor); - filter.setLoginFormUrl("/login.jsp"); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "/login.jsp")); MockHttpServletResponse response = new MockHttpServletResponse(); filter.doFilter(request, response, chain); diff --git a/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPointTests.java b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPointTests.java new file mode 100644 index 0000000000..3eb9913f93 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterEntryPointTests.java @@ -0,0 +1,82 @@ +/* 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.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; + + +/** + * Tests {@link BasicProcessingFilterEntryPoint}. + * + * @author Ben Alex + * @version $Id$ + */ +public class BasicProcessingFilterEntryPointTests extends TestCase { + //~ Constructors =========================================================== + + public BasicProcessingFilterEntryPointTests() { + super(); + } + + public BasicProcessingFilterEntryPointTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(BasicProcessingFilterEntryPointTests.class); + } + + public void testDetectsMissingRealmName() throws Exception { + BasicProcessingFilterEntryPoint ep = new BasicProcessingFilterEntryPoint(); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("realmName must be specified", expected.getMessage()); + } + } + + public void testGettersSetters() { + BasicProcessingFilterEntryPoint ep = new BasicProcessingFilterEntryPoint(); + ep.setRealmName("realm"); + assertEquals("realm", ep.getRealmName()); + } + + public void testNormalOperation() throws Exception { + BasicProcessingFilterEntryPoint ep = new BasicProcessingFilterEntryPoint(); + ep.setRealmName("hello"); + + MockHttpServletRequest request = new MockHttpServletRequest( + "/some_path"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + ep.afterPropertiesSet(); + ep.commence(request, response); + assertEquals(401, response.getError()); + assertEquals("Basic realm=\"hello\"", + response.getHeader("WWW-Authenticate")); + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java index d61238e2c4..a1fa7dede5 100644 --- a/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java +++ b/core/src/test/java/org/acegisecurity/ui/basicauth/BasicProcessingFilterTests.java @@ -18,6 +18,7 @@ package net.sf.acegisecurity.ui.basicauth; import junit.framework.TestCase; import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.MockAuthenticationEntryPoint; import net.sf.acegisecurity.MockAuthenticationManager; import net.sf.acegisecurity.MockFilterConfig; import net.sf.acegisecurity.MockHttpServletRequest; @@ -130,6 +131,10 @@ public class BasicProcessingFilterTests extends TestCase { BasicProcessingFilter filter = new BasicProcessingFilter(); filter.setAuthenticationManager(new MockAuthenticationManager()); assertTrue(filter.getAuthenticationManager() != null); + + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "sx")); + assertTrue(filter.getAuthenticationEntryPoint() != null); } public void testInvalidBasicAuthorizationTokenIsIgnored() @@ -228,10 +233,25 @@ public class BasicProcessingFilterTests extends TestCase { assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); } + public void testStartupDetectsMissingAuthenticationEntryPoint() + throws Exception { + try { + BasicProcessingFilter filter = new BasicProcessingFilter(); + filter.setAuthenticationManager(new MockAuthenticationManager()); + filter.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("An AuthenticationEntryPoint is required", + expected.getMessage()); + } + } + public void testStartupDetectsMissingAuthenticationManager() throws Exception { try { BasicProcessingFilter filter = new BasicProcessingFilter(); + filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint( + "x")); filter.afterPropertiesSet(); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { @@ -293,7 +313,7 @@ public class BasicProcessingFilterTests extends TestCase { chain); assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); - assertEquals(403, response.getError()); + assertEquals(401, response.getError()); } public void testWrongPasswordReturnsForbidden() throws Exception { @@ -325,7 +345,7 @@ public class BasicProcessingFilterTests extends TestCase { chain); assertTrue(request.getSession().getAttribute(HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY) == null); - assertEquals(403, response.getError()); + assertEquals(401, response.getError()); } private void executeFilterInContainerSimulator(FilterConfig filterConfig, diff --git a/core/src/test/java/org/acegisecurity/ui/basicauth/filtertest-valid.xml b/core/src/test/java/org/acegisecurity/ui/basicauth/filtertest-valid.xml index cc13218cfc..08c20c8af0 100644 --- a/core/src/test/java/org/acegisecurity/ui/basicauth/filtertest-valid.xml +++ b/core/src/test/java/org/acegisecurity/ui/basicauth/filtertest-valid.xml @@ -49,6 +49,11 @@ + + + + + Test Suite Realm diff --git a/core/src/test/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java b/core/src/test/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java new file mode 100644 index 0000000000..140991f238 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/webapp/AuthenticationProcessingFilterEntryPointTests.java @@ -0,0 +1,80 @@ +/* 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.webapp; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; + + +/** + * Tests {@link AuthenticationProcessingFilterEntryPoint}. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationProcessingFilterEntryPointTests extends TestCase { + //~ Constructors =========================================================== + + public AuthenticationProcessingFilterEntryPointTests() { + super(); + } + + public AuthenticationProcessingFilterEntryPointTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(AuthenticationProcessingFilterEntryPointTests.class); + } + + public void testDetectsMissingLoginFormUrl() throws Exception { + AuthenticationProcessingFilterEntryPoint ep = new AuthenticationProcessingFilterEntryPoint(); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("loginFormUrl must be specified", expected.getMessage()); + } + } + + public void testGettersSetters() { + AuthenticationProcessingFilterEntryPoint ep = new AuthenticationProcessingFilterEntryPoint(); + ep.setLoginFormUrl("/hello"); + assertEquals("/hello", ep.getLoginFormUrl()); + } + + public void testNormalOperation() throws Exception { + AuthenticationProcessingFilterEntryPoint ep = new AuthenticationProcessingFilterEntryPoint(); + ep.setLoginFormUrl("/hello"); + + MockHttpServletRequest request = new MockHttpServletRequest( + "/some_path"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + ep.afterPropertiesSet(); + ep.commence(request, response); + assertEquals("/hello", response.getRedirect()); + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 823af44c9f..851885eeb1 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -538,11 +538,15 @@ so you should configure a ContextLoaderListener in web.xml. - In the application context you will need to configure two + In the application context you will need to configure three beans: <bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> <property name="filterSecurityInterceptor"><ref bean="filterInvocationInterceptor"/></property> + <property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property> +</bean> + +<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"> <property name="loginFormUrl"><value>/acegilogin.jsp</value></property> </bean> @@ -559,16 +563,21 @@ </property> </bean> - The loginFormUrl is where the filter will - redirect the user's browser if they request a secure HTTP resource but - they are not authenticated. If the user is authenticated, a "403 - Forbidden" response will be returned to the browser. All paths are - relative to the web application root. + The AuthenticationEntryPoint will be called + if the user requests a secure HTTP resource but they are not + authenticated. The class handles presenting the appropriate response + to the user so that authentication can begin. Two concrete + implementations are provided with the Acegi Security System for + Spring: AuthenticationProcessingFilterEntryPoint + for commencing a form-based authentication, and + BasicProcessingFilterEntryPoint for commencing a + Http Basic authentication process. The SecurityEnforcementFilter primarily - provides redirection and session management support. It delegates - actual FilterInvocation security decisions to the - configured FilterSecurityInterceptor. + provides session management support and initiates authentication when + required. It delegates actual FilterInvocation + security decisions to the configured + FilterSecurityInterceptor. Like any other security interceptor, the FilterSecurityInterceptor requires a reference to @@ -1560,19 +1569,18 @@ public boolean supports(Class clazz); 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 + 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 + processing authentication credentials presented in HTTP headers. This + can be used for authenticating calls made by Spring remoting protocols + (such as Hessian and Burlap), as well as normal user agents (such as + Internet Explorer and Navigator). 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, behind a - FilterToBeanProxy: + the following filter to web.xml: <filter> <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name> @@ -1591,16 +1599,25 @@ public boolean supports(Class clazz); For a discussion of FilterToBeanProxy, please refer to the FilterInvocation Security Interceptor section. The application context will need to define the - BasicProcessingFilter: + BasicProcessingFilter and its required + collaborator: <bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"> <property name="authenticationManager"><ref bean="authenticationManager"/></property> + <property name="authenticationEntryPoint"><ref bean="authenticationEntryPoint"/></property> +</bean> + +<bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"> + <property name="realmName"><value>Name Of Your Realm</value></property> </bean> The configured 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 + processes each authentication request. If authentication fails, the + configured AuthenticationEntryPoint will be used to + retry the authentication process. Usually you will use the + BasicProcessingFilterEntryPoint, which returns a + 401 response with a suitable header to retry HTTP Basic + authentication. If authentication is successful, the resulting Authentication object will be placed into the HttpSession attribute indicated by HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY. @@ -1611,13 +1628,14 @@ public boolean supports(Class clazz); 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. + fails and the AuthenticationEntryPoint is called, + 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. + Authentication, as demonstrated in the Contacts sample application. + You can also use it instead of HTTP Session Authentication if you + wish. diff --git a/samples/contacts/etc/ca/applicationContext.xml b/samples/contacts/etc/ca/applicationContext.xml index b785c9c100..46802b2516 100644 --- a/samples/contacts/etc/ca/applicationContext.xml +++ b/samples/contacts/etc/ca/applicationContext.xml @@ -52,6 +52,11 @@ + + + + + Contacts Realm diff --git a/samples/contacts/etc/filter/applicationContext.xml b/samples/contacts/etc/filter/applicationContext.xml index 213241f855..08209c45a9 100644 --- a/samples/contacts/etc/filter/applicationContext.xml +++ b/samples/contacts/etc/filter/applicationContext.xml @@ -47,6 +47,11 @@ + + + + + Contacts Realm @@ -138,6 +143,10 @@ + + + + /acegilogin.jsp