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 1f5419448f..8340940bb0 100644
--- a/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java
+++ b/core/src/main/java/org/acegisecurity/ui/basicauth/BasicProcessingFilter.java
@@ -19,7 +19,6 @@ import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.AuthenticationException;
import net.sf.acegisecurity.AuthenticationManager;
import net.sf.acegisecurity.context.ContextHolder;
-import net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter;
import net.sf.acegisecurity.context.security.SecureContext;
import net.sf.acegisecurity.context.security.SecureContextUtils;
import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
@@ -46,7 +45,7 @@ import javax.servlet.http.HttpServletResponse;
/**
* Processes a HTTP request's BASIC authorization headers, putting the result
- * into the HttpSession.
+ * into the ContextHolder.
*
*
* For a detailed background on what this filter is designed to process, refer @@ -75,9 +74,7 @@ import javax.servlet.http.HttpServletResponse; * *
* If authentication is successful, the resulting {@link Authentication} object
- * will be placed into the HttpSession with the attribute defined
- * by {@link
- * HttpSessionContextIntegrationFilter#ACEGI_SECURITY_AUTHENTICATION_KEY}.
+ * will be placed into the ContextHolder.
*
@@ -87,6 +84,15 @@ import javax.servlet.http.HttpServletResponse; *
* *+ * Basic authentication is an attractive protocol because it is simple and + * widely deployed. However, it still transmits a password in clear text and + * as such is undesirable in many situations. Digest authentication is also + * provided by Acegi Security and should be used instead of Basic + * authentication wherever possible. See {@link + * net.sf.acegisecurity.ui.digestauth.DigestProcessingFilter}. + *
+ * + *
* Do not use this class directly. Instead configure
* web.xml to use the {@link
* net.sf.acegisecurity.util.FilterToBeanProxy}.
diff --git a/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilter.java
new file mode 100644
index 0000000000..9529b5c5d4
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilter.java
@@ -0,0 +1,452 @@
+/* Copyright 2004, 2005 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.digestauth;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationServiceException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.context.ContextHolder;
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+import net.sf.acegisecurity.providers.dao.UserCache;
+import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
+import net.sf.acegisecurity.providers.dao.cache.NullUserCache;
+import net.sf.acegisecurity.ui.WebAuthenticationDetails;
+import net.sf.acegisecurity.util.StringSplitUtils;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.StringUtils;
+
+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 Digest authorization headers, putting the result
+ * into the ContextHolder.
+ *
+ *
+ * For a detailed background on what this filter is designed to process, refer + * to RFC 2617 (which + * superseeded RFC 2069, although this filter support clients that implement + * either RFC 2617 or RFC 2069). + *
+ * + *+ * This filter can be used to provide Digest authentication services to both + * remoting protocol clients (such as Hessian and SOAP) as well as standard + * user agents (such as Internet Explorer and FireFox). + *
+ * + *+ * This Digest implementation has been designed to avoid needing to store + * session state between invocations. All session management information is + * stored in the "nonce" that is sent to the client by the {@link + * net.sf.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint}. + *
+ * + *
+ * If authentication is successful, the resulting {@link Authentication} object
+ * will be placed into the ContextHolder.
+ *
+ * If authentication fails, an {@link AuthenticationEntryPoint} implementation + * is called. This must always be {@link DigestProcessingFilterEntryPoint}, + * which will prompt the user to authenticate again via Digest authentication. + *
+ * + *+ * Note there are limitations to Digest authentication, although it is a more + * comprehensive and secure solution than Basic authentication. Please see RFC + * 2617 section 4 for a full discussion on the advantages of Digest + * authentication over Basic authentication, including commentary on the + * limitations that it still imposes. + *
+ * + *
+ * Do not use this class directly. Instead configure
+ * web.xml to use the {@link
+ * net.sf.acegisecurity.util.FilterToBeanProxy}.
+ *
response portion of a Digest authentication
+ * header. Both the server and user agent should compute the
+ * response independently. Provided as a static method to
+ * simply the coding of user agents.
+ *
+ * @param username DOCUMENT ME!
+ * @param realm DOCUMENT ME!
+ * @param password DOCUMENT ME!
+ * @param httpMethod DOCUMENT ME!
+ * @param uri DOCUMENT ME!
+ * @param qop DOCUMENT ME!
+ * @param nonce DOCUMENT ME!
+ * @param nc DOCUMENT ME!
+ * @param cnonce DOCUMENT ME!
+ *
+ * @return the MD5 of the digest authentication response, encoded in hex
+ *
+ * @throws IllegalArgumentException DOCUMENT ME!
+ */
+ public static String generateDigest(String username, String realm,
+ String password, String httpMethod, String uri, String qop,
+ String nonce, String nc, String cnonce) throws IllegalArgumentException {
+ String a1 = username + ":" + realm + ":" + password;
+ String a2 = httpMethod + ":" + uri;
+ String a1Md5 = new String(DigestUtils.md5Hex(a1));
+ String a2Md5 = new String(DigestUtils.md5Hex(a2));
+
+ String digest;
+
+ if (qop == null) {
+ // as per RFC 2069 compliant clients (also reaffirmed by RFC 2617)
+ digest = a1Md5 + ":" + nonce + ":" + a2Md5;
+ } else if ("auth".equals(qop)) {
+ // As per RFC 2617 compliant clients
+ digest = a1Md5 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop
+ + ":" + a2Md5;
+ } else {
+ throw new IllegalArgumentException(
+ "This method does not support a qop: '" + qop + "'");
+ }
+
+ String digestMd5 = new String(DigestUtils.md5Hex(digest));
+
+ return digestMd5;
+ }
+
+ public void init(FilterConfig arg0) throws ServletException {}
+
+ private void fail(ServletRequest request, ServletResponse response,
+ AuthenticationException failed) throws IOException, ServletException {
+ SecureContext sc = SecureContextUtils.getSecureContext();
+ sc.setAuthentication(null);
+ ContextHolder.setContext(sc);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(failed);
+ }
+
+ authenticationEntryPoint.commence(request, response, failed);
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPoint.java b/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPoint.java
new file mode 100644
index 0000000000..5815ab56b0
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPoint.java
@@ -0,0 +1,137 @@
+/* Copyright 2004, 2005 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.digestauth;
+
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+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 DigestProcessingFilter}.
+ *
+ *
+ * The nonce sent back to the user agent will be valid for the period indicated
+ * by {@link #setNonceValiditySeconds(int)}. By default this is 300 seconds.
+ * Shorter times should be used if replay attacks are a major concern. Larger
+ * values can be used if performance is a greater concern. This class
+ * correctly presents the stale=true header when the nonce has
+ * expierd, so properly implemented user agents will automatically renegotiate
+ * with a new nonce value (ie without presenting a new password dialog box to
+ * the user).
+ *
NonceExpiredException with the specified
+ * message.
+ *
+ * @param msg the detail message
+ */
+ public NonceExpiredException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a NonceExpiredException with the specified
+ * message and root cause.
+ *
+ * @param msg the detail message
+ * @param t root cause
+ */
+ public NonceExpiredException(String msg, Throwable t) {
+ super(msg, t);
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/ui/digestauth/package.html b/core/src/main/java/org/acegisecurity/ui/digestauth/package.html
new file mode 100644
index 0000000000..a6ba011cc2
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/ui/digestauth/package.html
@@ -0,0 +1,5 @@
+
+
+Authenticates HTTP Digest authentication requests.
+
+
diff --git a/core/src/main/java/org/acegisecurity/util/StringSplitUtils.java b/core/src/main/java/org/acegisecurity/util/StringSplitUtils.java
new file mode 100644
index 0000000000..3588713a33
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/util/StringSplitUtils.java
@@ -0,0 +1,123 @@
+/* Copyright 2004, 2005 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.util;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Provides several String manipulation methods.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class StringSplitUtils {
+ //~ Methods ================================================================
+
+ /**
+ * Splits a String at the first instance of the delimiter.
+ *
+ * + * Does not include the delimiter in the response. + *
+ * + * @param toSplit the string to split + * @param delimiter to split the string up with + * + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the + * delimiter) + * + * @throws IllegalArgumentException if an argument was invalid + */ + public static String[] split(String toSplit, String delimiter) { + Assert.hasLength(toSplit, "Cannot split a null or empty string"); + Assert.hasLength(delimiter, + "Cannot use a null or empty delimiter to split a string"); + + if (delimiter.length() != 1) { + throw new IllegalArgumentException( + "Delimiter can only be one character in length"); + } + + int offset = toSplit.indexOf('='); + + if (offset < 0) { + return null; + } + + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + 1); + + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Takes an array ofStrings, and for each element removes any
+ * instances of removeCharacter, and splits the element based
+ * on the delimiter. A Map is then generated,
+ * with the left of the delimiter providing the key, and the right of the
+ * delimiter providing the value.
+ *
+ *
+ * Will trim both the key and value before adding to the Map.
+ *
null if no removal should
+ * occur
+ *
+ * @return a Map representing the array contents, or
+ * null if the array to process was null or empty
+ */
+ public static Map splitEachArrayElementAndCreateMap(String[] array,
+ String delimiter, String removeCharacters) {
+ if ((array == null) || (array.length == 0)) {
+ return null;
+ }
+
+ Map map = new HashMap();
+
+ for (int i = 0; i < array.length; i++) {
+ String postRemove;
+
+ if (removeCharacters == null) {
+ postRemove = array[i];
+ } else {
+ postRemove = StringUtils.replace(array[i], removeCharacters, "");
+ }
+
+ String[] splitThisArrayElement = split(postRemove, delimiter);
+
+ if (splitThisArrayElement == null) {
+ continue;
+ }
+
+ map.put(splitThisArrayElement[0].trim(),
+ splitThisArrayElement[1].trim());
+ }
+
+ return map;
+ }
+}
diff --git a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java
index eea99f4cd0..145d98e287 100644
--- a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java
+++ b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java
@@ -171,7 +171,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
}
public String getMethod() {
- throw new UnsupportedOperationException("mock method not implemented");
+ return "GET";
}
public void setParameter(String arg0, String value) {
diff --git a/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPointTests.java b/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPointTests.java
new file mode 100644
index 0000000000..748c4c979b
--- /dev/null
+++ b/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterEntryPointTests.java
@@ -0,0 +1,172 @@
+/* Copyright 2004, 2005 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.digestauth;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.DisabledException;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+import net.sf.acegisecurity.util.StringSplitUtils;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+
+
+/**
+ * Tests {@link DigestProcessingFilterEntryPoint}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class DigestProcessingFilterEntryPointTests extends TestCase {
+ //~ Constructors ===========================================================
+
+ public DigestProcessingFilterEntryPointTests() {
+ super();
+ }
+
+ public DigestProcessingFilterEntryPointTests(String arg0) {
+ super(arg0);
+ }
+
+ //~ Methods ================================================================
+
+ public final void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public static void main(String[] args) {
+ junit.textui.TestRunner.run(DigestProcessingFilterEntryPointTests.class);
+ }
+
+ public void testDetectsMissingKey() throws Exception {
+ DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
+ ep.setRealmName("realm");
+
+ try {
+ ep.afterPropertiesSet();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("key must be specified", expected.getMessage());
+ }
+ }
+
+ public void testDetectsMissingRealmName() throws Exception {
+ DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
+ ep.setKey("dcdc");
+ ep.setNonceValiditySeconds(12);
+
+ try {
+ ep.afterPropertiesSet();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("realmName must be specified", expected.getMessage());
+ }
+ }
+
+ public void testGettersSetters() {
+ DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
+ assertEquals(300, ep.getNonceValiditySeconds()); // 5 mins default
+ ep.setRealmName("realm");
+ assertEquals("realm", ep.getRealmName());
+ ep.setKey("dcdc");
+ assertEquals("dcdc", ep.getKey());
+ ep.setNonceValiditySeconds(12);
+ assertEquals(12, ep.getNonceValiditySeconds());
+ }
+
+ public void testNormalOperation() throws Exception {
+ DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
+ ep.setRealmName("hello");
+ ep.setKey("key");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(
+ "/some_path");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ ep.afterPropertiesSet();
+
+ ep.commence(request, response, new DisabledException("foobar"));
+
+ // Check response is properly formed
+ assertEquals(401, response.getError());
+ assertTrue(response.getHeader("WWW-Authenticate").startsWith("Digest "));
+
+ // Break up response header
+ String header = response.getHeader("WWW-Authenticate").substring(7);
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", "\"");
+
+ assertEquals("hello", headerMap.get("realm"));
+ assertEquals("auth", headerMap.get("qop"));
+ assertNull(headerMap.get("stale"));
+
+ checkNonceValid((String) headerMap.get("nonce"));
+ }
+
+ public void testOperationIfDueToStaleNonce() throws Exception {
+ DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
+ ep.setRealmName("hello");
+ ep.setKey("key");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(
+ "/some_path");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ ep.afterPropertiesSet();
+
+ ep.commence(request, response,
+ new NonceExpiredException("expired nonce"));
+
+ // Check response is properly formed
+ assertEquals(401, response.getError());
+ assertTrue(response.getHeader("WWW-Authenticate").startsWith("Digest "));
+
+ // Break up response header
+ String header = response.getHeader("WWW-Authenticate").substring(7);
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", "\"");
+
+ assertEquals("hello", headerMap.get("realm"));
+ assertEquals("auth", headerMap.get("qop"));
+ assertEquals("true", headerMap.get("stale"));
+
+ checkNonceValid((String) headerMap.get("nonce"));
+ }
+
+ private void checkNonceValid(String nonce) {
+ // Check the nonce seems to be generated correctly
+ // format of nonce is:
+ // base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
+ assertTrue(Base64.isArrayByteBase64(nonce.getBytes()));
+
+ String decodedNonce = new String(Base64.decodeBase64(nonce.getBytes()));
+ String[] nonceTokens = StringUtils.delimitedListToStringArray(decodedNonce,
+ ":");
+ assertEquals(2, nonceTokens.length);
+
+ String expectedNonceSignature = DigestUtils.md5Hex(nonceTokens[0] + ":"
+ + "key");
+ assertEquals(expectedNonceSignature, nonceTokens[1]);
+ }
+}
diff --git a/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterTests.java
new file mode 100644
index 0000000000..ddb55f6fe9
--- /dev/null
+++ b/core/src/test/java/org/acegisecurity/ui/digestauth/DigestProcessingFilterTests.java
@@ -0,0 +1,887 @@
+/* Copyright 2004, 2005 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.digestauth;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.DisabledException;
+import net.sf.acegisecurity.MockFilterConfig;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+import net.sf.acegisecurity.MockHttpSession;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.context.ContextHolder;
+import net.sf.acegisecurity.context.security.SecureContextImpl;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
+import net.sf.acegisecurity.util.StringSplitUtils;
+
+import org.apache.commons.codec.binary.Base64;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import org.springframework.dao.DataAccessException;
+
+import org.springframework.util.StringUtils;
+
+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 DigestProcessingFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class DigestProcessingFilterTests extends TestCase {
+ //~ Constructors ===========================================================
+
+ public DigestProcessingFilterTests() {
+ super();
+ }
+
+ public DigestProcessingFilterTests(String arg0) {
+ super(arg0);
+ }
+
+ //~ Methods ================================================================
+
+ public static void main(String[] args) {
+ junit.textui.TestRunner.run(DigestProcessingFilterTests.class);
+ }
+
+ public void testDoFilterWithNonHttpServletRequestDetected()
+ throws Exception {
+ DigestProcessingFilter filter = new DigestProcessingFilter();
+
+ 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 {
+ DigestProcessingFilter filter = new DigestProcessingFilter();
+
+ 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 testExpiredNonceReturnsForbiddenWithStaleHeader()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(0);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+
+ String header = response.getHeader("WWW-Authenticate").substring(7);
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", "\"");
+ assertEquals("true", headerMap.get("stale"));
+ }
+
+ 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");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ }
+
+ public void testGettersSetters() {
+ DigestProcessingFilter filter = new DigestProcessingFilter();
+ filter.setAuthenticationDao(new MockAuthenticationDao());
+ assertTrue(filter.getAuthenticationDao() != null);
+
+ filter.setAuthenticationEntryPoint(new DigestProcessingFilterEntryPoint());
+ assertTrue(filter.getAuthenticationEntryPoint() != null);
+ }
+
+ public void testInvalidDigestAuthorizationTokenGeneratesError()
+ throws Exception {
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON";
+ headers.put("Authorization",
+ "Digest " + new String(Base64.encodeBase64(token.getBytes())));
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(false);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+ assertEquals(401, response.getError());
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ }
+
+ public void testMalformedHeaderReturnsForbidden() throws Exception {
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization", "Digest scsdcsdc");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testNonBase64EncodedNonceReturnsForbidden()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = "NOT_BASE_64_ENCODED";
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testNonceWithIncorrectSignatureForNumericFieldReturnsForbidden()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = new String(Base64.encodeBase64(
+ "123456:incorrectStringPassword".getBytes()));
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(false);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testNonceWithNonNumericFirstElementReturnsForbidden()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = new String(Base64.encodeBase64(
+ "hello:ignoredSecondElement".getBytes()));
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testNonceWithoutTwoColonSeparatedElementsReturnsForbidden()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = new String(Base64.encodeBase64(
+ "a base 64 string without a colon".getBytes()));
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testNormalOperation() throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNotNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals("marissa",
+ ((UserDetails) SecureContextUtils.getSecureContext()
+ .getAuthentication().getPrincipal())
+ .getUsername());
+ }
+
+ 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");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ }
+
+ public void testStartupDetectsMissingAuthenticationDao()
+ throws Exception {
+ try {
+ DigestProcessingFilter filter = new DigestProcessingFilter();
+ filter.setAuthenticationEntryPoint(new DigestProcessingFilterEntryPoint());
+ filter.afterPropertiesSet();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("An AuthenticationDao is required",
+ expected.getMessage());
+ }
+ }
+
+ public void testStartupDetectsMissingAuthenticationEntryPoint()
+ throws Exception {
+ try {
+ DigestProcessingFilter filter = new DigestProcessingFilter();
+ filter.setAuthenticationDao(new MockAuthenticationDao());
+ filter.afterPropertiesSet();
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertEquals("A DigestProcessingFilterEntryPoint is required",
+ expected.getMessage());
+ }
+ }
+
+ public void testSuccessLoginThenFailureLoginResultsInSessionLoosingToken()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNotNull(SecureContextUtils.getSecureContext().getAuthentication());
+
+ // Now retry, giving an invalid nonce
+ password = "WRONG_PASSWORD";
+ responseDigest = DigestProcessingFilter.generateDigest(username, realm,
+ password, "GET", uri, qop, nonce, nc, cnonce);
+
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ request = new MockHttpServletRequest(headers, null,
+ new MockHttpSession());
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ // Check we lost our previous authentication
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testWrongCnonceBasedOnDigestReturnsForbidden()
+ throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "NOT_SAME_AS_USED_FOR_DIGEST_COMPUTATION";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, "DIFFERENT_CNONCE");
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testWrongDigestReturnsForbidden() throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "WRONG_PASSWORD";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testWrongRealmReturnsForbidden() throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "marissa";
+ String realm = "WRONG_REALM";
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ public void testWrongUsernameReturnsForbidden() throws Exception {
+ Map responseHeaderMap = generateValidHeaders(60);
+
+ String username = "NOT_A_KNOWN_USER";
+ String realm = (String) responseHeaderMap.get("realm");
+ String nonce = (String) responseHeaderMap.get("nonce");
+ String uri = "/some_file.html";
+ String qop = (String) responseHeaderMap.get("qop");
+ String nc = "00000002";
+ String cnonce = "c822c727a648aba7";
+ String password = "koala";
+ String responseDigest = DigestProcessingFilter.generateDigest(username,
+ realm, password, "GET", uri, qop, nonce, nc, cnonce);
+
+ // Setup our HTTP request
+ Map headers = new HashMap();
+ headers.put("Authorization",
+ "Digest username=\"" + username + "\", realm=\"" + realm
+ + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", response=\""
+ + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\""
+ + cnonce + "\"");
+
+ MockHttpServletRequest request = new MockHttpServletRequest(headers,
+ null, new MockHttpSession());
+ request.setServletPath("/some_file.html");
+
+ // Launch an application context and access our bean
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilter filter = (DigestProcessingFilter) ctx.getBean(
+ "digestProcessingFilter");
+
+ // Setup our filter configuration
+ MockFilterConfig config = new MockFilterConfig();
+
+ // Setup our expectation that the filter chain will be invoked
+ MockFilterChain chain = new MockFilterChain(true);
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ // Test
+ executeFilterInContainerSimulator(config, filter, request, response,
+ chain);
+
+ assertNull(SecureContextUtils.getSecureContext().getAuthentication());
+ assertEquals(401, response.getError());
+ }
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ ContextHolder.setContext(new SecureContextImpl());
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ ContextHolder.setContext(null);
+ }
+
+ 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();
+ }
+
+ private Map generateValidHeaders(int nonceValidityPeriod)
+ throws Exception {
+ ApplicationContext ctx = new ClassPathXmlApplicationContext(
+ "net/sf/acegisecurity/ui/digestauth/filtertest-valid.xml");
+ DigestProcessingFilterEntryPoint ep = (DigestProcessingFilterEntryPoint) ctx
+ .getBean("digestProcessingFilterEntryPoint");
+ ep.setNonceValiditySeconds(nonceValidityPeriod);
+
+ MockHttpServletRequest request = new MockHttpServletRequest(
+ "/some_path");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ ep.commence(request, response, new DisabledException("foobar"));
+
+ // Break up response header
+ String header = response.getHeader("WWW-Authenticate").substring(7);
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(header);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", "\"");
+
+ return headerMap;
+ }
+
+ //~ Inner Classes ==========================================================
+
+ private class MockAuthenticationDao implements AuthenticationDao {
+ public UserDetails loadUserByUsername(String username)
+ throws UsernameNotFoundException, DataAccessException {
+ return null;
+ }
+ }
+
+ 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/core/src/test/java/org/acegisecurity/util/StringSplitUtilsTests.java b/core/src/test/java/org/acegisecurity/util/StringSplitUtilsTests.java
new file mode 100644
index 0000000000..09fc3df48b
--- /dev/null
+++ b/core/src/test/java/org/acegisecurity/util/StringSplitUtilsTests.java
@@ -0,0 +1,140 @@
+/* Copyright 2004, 2005 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.util;
+
+import junit.framework.TestCase;
+
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+
+
+/**
+ * Tests {@link net.sf.acegisecurity.util.StringSplitUtils}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class StringSplitUtilsTests extends TestCase {
+ //~ Constructors ===========================================================
+
+ // ===========================================================
+ public StringSplitUtilsTests() {
+ super();
+ }
+
+ public StringSplitUtilsTests(String arg0) {
+ super(arg0);
+ }
+
+ //~ Methods ================================================================
+
+ // ================================================================
+ public static void main(String[] args) {
+ junit.textui.TestRunner.run(StringSplitUtilsTests.class);
+ }
+
+ public void testSplitEachArrayElementAndCreateMapNormalOperation() {
+ // note it ignores malformed entries (ie those without an equals sign)
+ String unsplit = "username=\"marissa\", invalidEntryThatHasNoEqualsSign, realm=\"Contacts Realm\", nonce=\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\", uri=\"/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\", response=\"38644211cf9ac3da63ab639807e2baff\", qop=auth, nc=00000004, cnonce=\"2b8d329a8571b99a\"";
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(unsplit);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", "\"");
+
+ assertEquals("marissa", headerMap.get("username"));
+ assertEquals("Contacts Realm", headerMap.get("realm"));
+ assertEquals("MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==",
+ headerMap.get("nonce"));
+ assertEquals("/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4",
+ headerMap.get("uri"));
+ assertEquals("38644211cf9ac3da63ab639807e2baff",
+ headerMap.get("response"));
+ assertEquals("auth", headerMap.get("qop"));
+ assertEquals("00000004", headerMap.get("nc"));
+ assertEquals("2b8d329a8571b99a", headerMap.get("cnonce"));
+ assertEquals(8, headerMap.size());
+ }
+
+ public void testSplitEachArrayElementAndCreateMapRespectsInstructionNotToRemoveCharacters() {
+ String unsplit = "username=\"marissa\", realm=\"Contacts Realm\", nonce=\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\", uri=\"/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\", response=\"38644211cf9ac3da63ab639807e2baff\", qop=auth, nc=00000004, cnonce=\"2b8d329a8571b99a\"";
+ String[] headerEntries = StringUtils.commaDelimitedListToStringArray(unsplit);
+ Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
+ "=", null);
+
+ assertEquals("\"marissa\"", headerMap.get("username"));
+ assertEquals("\"Contacts Realm\"", headerMap.get("realm"));
+ assertEquals("\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\"",
+ headerMap.get("nonce"));
+ assertEquals("\"/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\"",
+ headerMap.get("uri"));
+ assertEquals("\"38644211cf9ac3da63ab639807e2baff\"",
+ headerMap.get("response"));
+ assertEquals("auth", headerMap.get("qop"));
+ assertEquals("00000004", headerMap.get("nc"));
+ assertEquals("\"2b8d329a8571b99a\"", headerMap.get("cnonce"));
+ assertEquals(8, headerMap.size());
+ }
+
+ public void testSplitEachArrayElementAndCreateMapReturnsNullIfArrayEmptyOrNull() {
+ assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(null,
+ "=", "\""));
+ assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(
+ new String[] {}, "=", "\""));
+ }
+
+ public void testSplitNormalOperation() {
+ String unsplit = "username=\"marissa==\"";
+ assertEquals("username", StringSplitUtils.split(unsplit, "=")[0]);
+ assertEquals("\"marissa==\"", StringSplitUtils.split(unsplit, "=")[1]); // should not remove quotes or extra equals
+ }
+
+ public void testSplitRejectsNullsAndIncorrectLengthStrings() {
+ try {
+ StringSplitUtils.split(null, "="); // null
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+
+ try {
+ StringSplitUtils.split("", "="); // empty string
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+
+ try {
+ StringSplitUtils.split("sdch=dfgf", null); // null
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+
+ try {
+ StringSplitUtils.split("fvfv=dcdc", ""); // empty string
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+
+ try {
+ StringSplitUtils.split("dfdc=dcdc", "BIGGER_THAN_ONE_CHARACTER");
+ fail("Should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(true);
+ }
+ }
+}
diff --git a/core/src/test/resources/org/acegisecurity/ui/digestauth/filtertest-valid.xml b/core/src/test/resources/org/acegisecurity/ui/digestauth/filtertest-valid.xml
new file mode 100644
index 0000000000..eb1d789d9f
--- /dev/null
+++ b/core/src/test/resources/org/acegisecurity/ui/digestauth/filtertest-valid.xml
@@ -0,0 +1,61 @@
+
+
+
+
+