Jelajahi Sumber

Make SecurityEnforcementFilter support pluggable authentication entry points. Enhance BASIC authentication so it's a viable alternative to form-based authentication for user agents like IE and Netscape.

Ben Alex 21 tahun lalu
induk
melakukan
6815e693a7

+ 56 - 0
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.
+     * 
+     * <P>
+     * <code>SecurityEnforcementFilter</code> will populate the
+     * <code>HttpSession</code> attribute named
+     * <code>AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY</code>
+     * with the requested target URL before calling this method.
+     * </p>
+     * 
+     * <P>
+     * Implementations should modify the headers on the
+     * <code>ServletResponse</code> to as necessary to commence the
+     * authentication process.
+     * </p>
+     *
+     * @param request that resulted in an <code>AuthenticationException</code>
+     * @param response so that the user agent can begin authentication
+     */
+    public void commence(ServletRequest request, ServletResponse response)
+        throws IOException, ServletException;
+}

+ 19 - 23
core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java

@@ -59,8 +59,7 @@ import javax.servlet.http.HttpServletResponse;
  * </p>
  * 
  * <p>
- * 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:
  * </p>
  * 
  * <ul>
@@ -70,8 +69,9 @@ import javax.servlet.http.HttpServletResponse;
  * to.
  * </li>
  * <li>
- * <code>loginFormUrl</code> indicates the URL that should be used for
- * redirection if an <code>AuthenticationException</code> is detected.
+ * <code>authenticationEntryPoint</code> indicates the handler that should
+ * commence the authentication process if an
+ * <code>AuthenticationException</code> is detected.
  * </li>
  * </ul>
  * 
@@ -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
-     * <code>AuthenticationException</code> 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(

+ 23 - 8
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;
  * </p>
  * 
  * <p>
- * 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).
  * </p>
  * 
  * <P>
@@ -75,10 +76,9 @@ import javax.servlet.http.HttpServletResponse;
  * </p>
  * 
  * <p>
- * If authentication fails, a <code>HttpServletResponse.SC_FORBIDDEN</code>
- * (403 error) response is sent. This is consistent with RFC 1945, Section 11,
- * which states, "<I>If the server does not wish to accept the credentials
- * sent with a request, it should return a 403 (forbidden) response.</I>".
+ * 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.
  * </p>
  * 
  * <P>
@@ -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;
             }

+ 75 - 0
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 <code>SecurityEnforcementFilter</code> to commence
+ * authentication via the {@link BasicProcessingFilter}.
+ * 
+ * <P>
+ * 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.
+ * </p>
+ *
+ * @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);
+    }
+}

+ 69 - 0
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 <code>SecurityEnforcementFilter</code> to commence
+ * authentication via the {@link AuthenticationProcessingFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationProcessingFilterEntryPoint
+    implements AuthenticationEntryPoint, InitializingBean {
+    //~ Instance fields ========================================================
+
+    /**
+     * The URL where the <code>AuthenticationProcessingFilter</code> 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);
+    }
+}

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

+ 14 - 1
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) {

+ 19 - 12
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,31 +150,33 @@ public class SecurityEnforcementFilterTests extends TestCase {
             request.getSession().getAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY));
     }
 
-    public void testStartupDetectsMissingFilterSecurityInterceptor()
+    public void testStartupDetectsMissingAuthenticationEntryPoint()
         throws Exception {
         SecurityEnforcementFilter filter = new SecurityEnforcementFilter();
-        filter.setLoginFormUrl("/login.jsp");
+        filter.setFilterSecurityInterceptor(new MockFilterSecurityInterceptor(
+                false, false));
 
         try {
             filter.afterPropertiesSet();
             fail("Should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
-            assertEquals("filterSecurityInterceptor must be specified",
+            assertEquals("authenticationEntryPoint must be specified",
                 expected.getMessage());
         }
     }
 
-    public void testStartupDetectsMissingLoginFormUrl()
+    public void testStartupDetectsMissingFilterSecurityInterceptor()
         throws Exception {
         SecurityEnforcementFilter filter = new SecurityEnforcementFilter();
-        filter.setFilterSecurityInterceptor(new MockFilterSecurityInterceptor(
-                false, false));
+        filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint(
+                "/login.jsp"));
 
         try {
             filter.afterPropertiesSet();
             fail("Should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
-            assertEquals("loginFormUrl must be specified", expected.getMessage());
+            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);

+ 82 - 0
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"));
+    }
+}

+ 22 - 2
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,

+ 5 - 0
core/src/test/java/org/acegisecurity/ui/basicauth/filtertest-valid.xml

@@ -49,6 +49,11 @@
 
 	<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
 		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+		<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="basicProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+		<property name="realmName"><value>Test Suite Realm</value></property>
 	</bean>
 
 </beans>

+ 80 - 0
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());
+    }
+}

+ 42 - 24
docs/reference/src/index.xml

@@ -538,11 +538,15 @@
         so you should configure a <literal>ContextLoaderListener</literal> in
         <literal>web.xml</literal>.</para>
 
-        <para>In the application context you will need to configure two
+        <para>In the application context you will need to configure three
         beans:</para>
 
         <programlisting>&lt;bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"&gt;
   &lt;property name="filterSecurityInterceptor"&gt;&lt;ref bean="filterInvocationInterceptor"/&gt;&lt;/property&gt;
+  &lt;property name="authenticationEntryPoint"&gt;&lt;ref bean="authenticationEntryPoint"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"&gt;
   &lt;property name="loginFormUrl"&gt;&lt;value&gt;/acegilogin.jsp&lt;/value&gt;&lt;/property&gt;
 &lt;/bean&gt;
 
@@ -559,16 +563,21 @@
   &lt;/property&gt;
 &lt;/bean&gt;</programlisting>
 
-        <para>The <literal>loginFormUrl</literal> 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.</para>
+        <para>The <literal>AuthenticationEntryPoint</literal> 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: <literal>AuthenticationProcessingFilterEntryPoint</literal>
+        for commencing a form-based authentication, and
+        <literal>BasicProcessingFilterEntryPoint</literal> for commencing a
+        Http Basic authentication process.</para>
 
         <para>The <literal>SecurityEnforcementFilter</literal> primarily
-        provides redirection and session management support. It delegates
-        actual <literal>FilterInvocation</literal> security decisions to the
-        configured <literal>FilterSecurityInterceptor</literal>.</para>
+        provides session management support and initiates authentication when
+        required. It delegates actual <literal>FilterInvocation</literal>
+        security decisions to the configured
+        <literal>FilterSecurityInterceptor</literal>.</para>
 
         <para>Like any other security interceptor, the
         <literal>FilterSecurityInterceptor</literal> requires a reference to
@@ -1560,19 +1569,18 @@ public boolean supports(Class clazz);</programlisting></para>
       <sect2 id="security-ui-http-basic">
         <title>HTTP Basic Authentication</title>
 
-        <para>Primarily to cater for the needs of remoting protocols such as
-        Hessian and Burlap, the Acegi Security System for Spring provides a
+        <para>The Acegi Security System for Spring provides a
         <literal>BasicProcessingFilter</literal> 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
         <literal>BasicProcessingFilter</literal> conforms with this
         RFC.</para>
 
         <para>To implement HTTP Basic Authentication, it is necessary to add
-        the following filter to <literal>web.xml</literal>, behind a
-        <literal>FilterToBeanProxy</literal>:</para>
+        the following filter to <literal>web.xml</literal>:</para>
 
         <para><programlisting>&lt;filter&gt;
   &lt;filter-name&gt;Acegi HTTP BASIC Authorization Filter&lt;/filter-name&gt;
@@ -1591,16 +1599,25 @@ public boolean supports(Class clazz);</programlisting></para>
         <para>For a discussion of <literal>FilterToBeanProxy</literal>, please
         refer to the FilterInvocation Security Interceptor section. The
         application context will need to define the
-        <literal>BasicProcessingFilter</literal>:</para>
+        <literal>BasicProcessingFilter</literal> and its required
+        collaborator:</para>
 
         <para><programlisting>&lt;bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"&gt;
   &lt;property name="authenticationManager"&gt;&lt;ref bean="authenticationManager"/&gt;&lt;/property&gt;
+  &lt;property name="authenticationEntryPoint"&gt;&lt;ref bean="authenticationEntryPoint"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="authenticationEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"&gt;
+  &lt;property name="realmName"&gt;&lt;value&gt;Name Of Your Realm&lt;/value&gt;&lt;/property&gt;
 &lt;/bean&gt;</programlisting></para>
 
         <para>The configured <literal>AuthenticationManager</literal>
-        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 <literal>AuthenticationEntryPoint</literal> will be used to
+        retry the authentication process. Usually you will use the
+        <literal>BasicProcessingFilterEntryPoint</literal>, which returns a
+        401 response with a suitable header to retry HTTP Basic
+        authentication. If authentication is successful, the resulting
         <literal>Authentication</literal> object will be placed into the
         <literal>HttpSession</literal> attribute indicated by
         <literal>HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY</literal>.
@@ -1611,13 +1628,14 @@ public boolean supports(Class clazz);</programlisting></para>
         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.</para>
+        fails and the <literal>AuthenticationEntryPoint</literal> is called,
+        as discussed in the previous paragraph.</para>
 
         <para>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.</para>
+        Authentication, as demonstrated in the Contacts sample application.
+        You can also use it instead of HTTP Session Authentication if you
+        wish.</para>
       </sect2>
 
       <sect2 id="security-ui-well-known">

+ 5 - 0
samples/contacts/etc/ca/applicationContext.xml

@@ -52,6 +52,11 @@
 
 	<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
 		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+		<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="basicProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+		<property name="realmName"><value>Contacts Realm</value></property>
 	</bean>
 
 	<!-- ~~~~~~~~~~~~~~~~~~~~ AUTHORIZATION DEFINITIONS ~~~~~~~~~~~~~~~~~~~ -->

+ 9 - 0
samples/contacts/etc/filter/applicationContext.xml

@@ -47,6 +47,11 @@
 
 	<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
 		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+		<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="basicProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+		<property name="realmName"><value>Contacts Realm</value></property>
 	</bean>
 
 	<!-- ~~~~~~~~~~~~~~~~~~~~ AUTHORIZATION DEFINITIONS ~~~~~~~~~~~~~~~~~~~ -->
@@ -138,6 +143,10 @@
 
 	<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter">
 		<property name="filterSecurityInterceptor"><ref bean="filterInvocationInterceptor"/></property>
+		<property name="authenticationEntryPoint"><ref bean="authenticationProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="authenticationProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
 		<property name="loginFormUrl"><value>/acegilogin.jsp</value></property>
 	</bean>