SEC-506: Fix as suggested by reporter. Split the disgest header string ignoring separating commas which occur between quotes.

This commit is contained in:
Luke Taylor 2007-08-28 00:31:30 +00:00
parent 3f123e1478
commit c8077c5e87
4 changed files with 177 additions and 110 deletions

View File

@ -68,20 +68,20 @@ import javax.servlet.http.HttpServletResponse;
* <code>SecurityContextHolder</code>.<p>For a detailed background on what this filter is designed to process, * <code>SecurityContextHolder</code>.<p>For a detailed background on what this filter is designed to process,
* refer to <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a> (which superseded RFC 2069, although this * refer to <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a> (which superseded RFC 2069, although this
* filter support clients that implement either RFC 2617 or RFC 2069).</p> * filter support clients that implement either RFC 2617 or RFC 2069).</p>
* <p>This filter can be used to provide Digest authentication services to both remoting protocol clients (such as * <p>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).</p> * Hessian and SOAP) as well as standard user agents (such as Internet Explorer and FireFox).</p>
* <p>This Digest implementation has been designed to avoid needing to store session state between invocations. * <p>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 * All session management information is stored in the "nonce" that is sent to the client by the {@link
* DigestProcessingFilterEntryPoint}.</p> * DigestProcessingFilterEntryPoint}.</p>
* <P>If authentication is successful, the resulting {@link org.acegisecurity.Authentication Authentication} * <P>If authentication is successful, the resulting {@link org.acegisecurity.Authentication Authentication}
* object will be placed into the <code>SecurityContextHolder</code>.</p> * object will be placed into the <code>SecurityContextHolder</code>.</p>
* <p>If authentication fails, an {@link org.acegisecurity.ui.AuthenticationEntryPoint AuthenticationEntryPoint} * <p>If authentication fails, an {@link org.acegisecurity.ui.AuthenticationEntryPoint AuthenticationEntryPoint}
* implementation is called. This must always be {@link DigestProcessingFilterEntryPoint}, which will prompt the user * implementation is called. This must always be {@link DigestProcessingFilterEntryPoint}, which will prompt the user
* to authenticate again via Digest authentication.</p> * to authenticate again via Digest authentication.</p>
* <p>Note there are limitations to Digest authentication, although it is a more comprehensive and secure solution * <p>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 * 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.</p> * authentication over Basic authentication, including commentary on the limitations that it still imposes.</p>
* <p><b>Do not use this class directly.</b> Instead configure <code>web.xml</code> to use the {@link * <p><b>Do not use this class directly.</b> Instead configure <code>web.xml</code> to use the {@link
* org.acegisecurity.util.FilterToBeanProxy}.</p> * org.acegisecurity.util.FilterToBeanProxy}.</p>
*/ */
public class DigestProcessingFilter implements Filter, InitializingBean, MessageSourceAware { public class DigestProcessingFilter implements Filter, InitializingBean, MessageSourceAware {
@ -105,10 +105,11 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
Assert.notNull(authenticationEntryPoint, "A DigestProcessingFilterEntryPoint is required"); Assert.notNull(authenticationEntryPoint, "A DigestProcessingFilterEntryPoint is required");
} }
public void destroy() {} public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) { if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Can only process HttpServletRequest"); throw new ServletException("Can only process HttpServletRequest");
} }
@ -128,7 +129,7 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
if ((header != null) && header.startsWith("Digest ")) { if ((header != null) && header.startsWith("Digest ")) {
String section212response = header.substring(7); String section212response = header.substring(7);
String[] headerEntries = StringUtils.commaDelimitedListToStringArray(section212response); String[] headerEntries = StringSplitUtils.splitIgnoringQuotes(section212response, ',');
Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\"");
String username = (String) headerMap.get("username"); String username = (String) headerMap.get("username");
@ -144,12 +145,12 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
if ((username == null) || (realm == null) || (nonce == null) || (uri == null) || (response == null)) { if ((username == null) || (realm == null) || (nonce == null) || (uri == null) || (response == null)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("extracted username: '" + username + "'; realm: '" + username + "'; nonce: '" logger.debug("extracted username: '" + username + "'; realm: '" + username + "'; nonce: '"
+ username + "'; uri: '" + username + "'; response: '" + username + "'"); + username + "'; uri: '" + username + "'; response: '" + username + "'");
} }
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingMandatory", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingMandatory",
new Object[] {section212response}, "Missing mandatory digest value; received header {0}"))); new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
return; return;
} }
@ -162,8 +163,8 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
} }
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingAuth", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.missingAuth",
new Object[] {section212response}, "Missing mandatory digest value; received header {0}"))); new Object[]{section212response}, "Missing mandatory digest value; received header {0}")));
return; return;
} }
@ -172,9 +173,9 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
// Check realm name equals what we expected // Check realm name equals what we expected
if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) { if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectRealm", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectRealm",
new Object[] {realm, this.getAuthenticationEntryPoint().getRealmName()}, new Object[]{realm, this.getAuthenticationEntryPoint().getRealmName()},
"Response realm name '{0}' does not match system realm name of '{1}'"))); "Response realm name '{0}' does not match system realm name of '{1}'")));
return; return;
} }
@ -182,22 +183,22 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
// Check nonce was a Base64 encoded (as sent by DigestProcessingFilterEntryPoint) // Check nonce was a Base64 encoded (as sent by DigestProcessingFilterEntryPoint)
if (!Base64.isArrayByteBase64(nonce.getBytes())) { if (!Base64.isArrayByteBase64(nonce.getBytes())) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceEncoding", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceEncoding",
new Object[] {nonce}, "Nonce is not encoded in Base64; received nonce {0}"))); new Object[]{nonce}, "Nonce is not encoded in Base64; received nonce {0}")));
return; return;
} }
// Decode nonce from Base64 // Decode nonce from Base64
// format of nonce is: // format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) // base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
String nonceAsPlainText = new String(Base64.decodeBase64(nonce.getBytes())); String nonceAsPlainText = new String(Base64.decodeBase64(nonce.getBytes()));
String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":"); String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":");
if (nonceTokens.length != 2) { if (nonceTokens.length != 2) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotTwoTokens", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotTwoTokens",
new Object[] {nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}"))); new Object[]{nonceAsPlainText}, "Nonce should have yielded two tokens but was {0}")));
return; return;
} }
@ -209,9 +210,9 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
nonceExpiryTime = new Long(nonceTokens[0]).longValue(); nonceExpiryTime = new Long(nonceTokens[0]).longValue();
} catch (NumberFormatException nfe) { } catch (NumberFormatException nfe) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotNumeric", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotNumeric",
new Object[] {nonceAsPlainText}, new Object[]{nonceAsPlainText},
"Nonce token should have yielded a numeric first token, but was {0}"))); "Nonce token should have yielded a numeric first token, but was {0}")));
return; return;
} }
@ -222,8 +223,8 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
if (!expectedNonceSignature.equals(nonceTokens[1])) { if (!expectedNonceSignature.equals(nonceTokens[1])) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceCompromised", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceCompromised",
new Object[] {nonceAsPlainText}, "Nonce token compromised {0}"))); new Object[]{nonceAsPlainText}, "Nonce token compromised {0}")));
return; return;
} }
@ -241,15 +242,15 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
user = userDetailsService.loadUserByUsername(username); user = userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) { } catch (UsernameNotFoundException notFound) {
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
new Object[] {username}, "Username {0} not found"))); new Object[]{username}, "Username {0} not found")));
return; return;
} }
if (user == null) { if (user == null) {
throw new AuthenticationServiceException( throw new AuthenticationServiceException(
"AuthenticationDao returned null, which is an interface contract violation"); "AuthenticationDao returned null, which is an interface contract violation");
} }
userCache.putUserInCache(user); userCache.putUserInCache(user);
@ -266,7 +267,7 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
if (!serverDigestMd5.equals(responseDigest) && !loadedFromDao) { if (!serverDigestMd5.equals(responseDigest) && !loadedFromDao) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug( logger.debug(
"Digest comparison failure; trying to refresh user from DAO in case password had changed"); "Digest comparison failure; trying to refresh user from DAO in case password had changed");
} }
try { try {
@ -274,8 +275,8 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
} catch (UsernameNotFoundException notFound) { } catch (UsernameNotFoundException notFound) {
// Would very rarely happen, as user existed earlier // Would very rarely happen, as user existed earlier
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.usernameNotFound",
new Object[] {username}, "Username {0} not found"))); new Object[]{username}, "Username {0} not found")));
} }
userCache.putUserInCache(user); userCache.putUserInCache(user);
@ -289,12 +290,12 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
if (!serverDigestMd5.equals(responseDigest)) { if (!serverDigestMd5.equals(responseDigest)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Expected response: '" + serverDigestMd5 + "' but received: '" + responseDigest logger.debug("Expected response: '" + serverDigestMd5 + "' but received: '" + responseDigest
+ "'; is AuthenticationDao returning clear text passwords?"); + "'; is AuthenticationDao returning clear text passwords?");
} }
fail(request, response, fail(request, response,
new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectResponse", new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectResponse",
"Incorrect response"))); "Incorrect response")));
return; return;
} }
@ -305,15 +306,15 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
// but the request was otherwise appearing to be valid // but the request was otherwise appearing to be valid
if (nonceExpiryTime < System.currentTimeMillis()) { if (nonceExpiryTime < System.currentTimeMillis()) {
fail(request, response, fail(request, response,
new NonceExpiredException(messages.getMessage("DigestProcessingFilter.nonceExpired", new NonceExpiredException(messages.getMessage("DigestProcessingFilter.nonceExpired",
"Nonce has expired/timed out"))); "Nonce has expired/timed out")));
return; return;
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Authentication success for user: '" + username + "' with response: '" + responseDigest logger.debug("Authentication success for user: '" + username + "' with response: '" + responseDigest
+ "'"); + "'");
} }
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user, UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user,
@ -335,7 +336,7 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
} }
private void fail(ServletRequest request, ServletResponse response, AuthenticationException failed) private void fail(ServletRequest request, ServletResponse response, AuthenticationException failed)
throws IOException, ServletException { throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(null); SecurityContextHolder.getContext().setAuthentication(null);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@ -351,24 +352,22 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
* coding of user agents. * coding of user agents.
* *
* @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if * @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if
* it is plain text. * it is plain text.
* @param username the user's login name. * @param username the user's login name.
* @param realm the name of the realm. * @param realm the name of the realm.
* @param password the user's password in plaintext or ready-encoded. * @param password the user's password in plaintext or ready-encoded.
* @param httpMethod the HTTP request method (GET, POST etc.) * @param httpMethod the HTTP request method (GET, POST etc.)
* @param uri the request URI. * @param uri the request URI.
* @param qop the qop directive, or null if not set. * @param qop the qop directive, or null if not set.
* @param nonce the nonce supplied by the server * @param nonce the nonce supplied by the server
* @param nc the "nonce-count" as defined in RFC 2617. * @param nc the "nonce-count" as defined in RFC 2617.
* @param cnonce opaque string supplied by the client when qop is set. * @param cnonce opaque string supplied by the client when qop is set.
*
* @return the MD5 of the digest authentication response, encoded in hex * @return the MD5 of the digest authentication response, encoded in hex
*
* @throws IllegalArgumentException if the supplied qop value is unsupported. * @throws IllegalArgumentException if the supplied qop value is unsupported.
*/ */
public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password, public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password,
String httpMethod, String uri, String qop, String nonce, String nc, String cnonce) String httpMethod, String uri, String qop, String nonce, String nc, String cnonce)
throws IllegalArgumentException { throws IllegalArgumentException {
String a1Md5 = null; String a1Md5 = null;
String a2 = httpMethod + ":" + uri; String a2 = httpMethod + ":" + uri;
String a2Md5 = new String(DigestUtils.md5Hex(a2)); String a2Md5 = new String(DigestUtils.md5Hex(a2));
@ -408,7 +407,8 @@ public class DigestProcessingFilter implements Filter, InitializingBean, Message
return userDetailsService; return userDetailsService;
} }
public void init(FilterConfig ignored) throws ServletException {} public void init(FilterConfig ignored) throws ServletException {
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");

View File

@ -20,6 +20,8 @@ import org.springframework.util.StringUtils;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import java.util.List;
/** /**
@ -29,6 +31,9 @@ import java.util.Map;
* @version $Id$ * @version $Id$
*/ */
public final class StringSplitUtils { public final class StringSplitUtils {
//~ Static fields/initializers =====================================================================================
private static final String[] EMPTY_STRING_ARRAY = new String[0];
//~ Constructors =================================================================================================== //~ Constructors ===================================================================================================
private StringSplitUtils() { private StringSplitUtils() {
@ -40,12 +45,10 @@ public final class StringSplitUtils {
* Splits a <code>String</code> at the first instance of the delimiter.<p>Does not include the delimiter in * Splits a <code>String</code> at the first instance of the delimiter.<p>Does not include the delimiter in
* the response.</p> * the response.</p>
* *
* @param toSplit the string to split * @param toSplit the string to split
* @param delimiter to split the string up with * @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 * @return a two element array with index 0 being before the delimiter, and index 1 being after the delimiter
* (neither element includes the delimiter) * (neither element includes the delimiter)
*
* @throws IllegalArgumentException if an argument was invalid * @throws IllegalArgumentException if an argument was invalid
*/ */
public static String[] split(String toSplit, String delimiter) { public static String[] split(String toSplit, String delimiter) {
@ -65,7 +68,7 @@ public final class StringSplitUtils {
String beforeDelimiter = toSplit.substring(0, offset); String beforeDelimiter = toSplit.substring(0, offset);
String afterDelimiter = toSplit.substring(offset + 1); String afterDelimiter = toSplit.substring(offset + 1);
return new String[] {beforeDelimiter, afterDelimiter}; return new String[]{beforeDelimiter, afterDelimiter};
} }
/** /**
@ -74,11 +77,10 @@ public final class StringSplitUtils {
* then generated, with the left of the delimiter providing the key, and the right of the delimiter providing the * then generated, with the left of the delimiter providing the key, and the right of the delimiter providing the
* value.<p>Will trim both the key and value before adding to the <code>Map</code>.</p> * value.<p>Will trim both the key and value before adding to the <code>Map</code>.</p>
* *
* @param array the array to process * @param array the array to process
* @param delimiter to split each element using (typically the equals symbol) * @param delimiter to split each element using (typically the equals symbol)
* @param removeCharacters one or more characters to remove from each element prior to attempting the split * @param removeCharacters one or more characters to remove from each element prior to attempting the split
* operation (typically the quotation mark symbol) or <code>null</code> if no removal should occur * operation (typically the quotation mark symbol) or <code>null</code> if no removal should occur
*
* @return a <code>Map</code> representing the array contents, or <code>null</code> if the array to process was * @return a <code>Map</code> representing the array contents, or <code>null</code> if the array to process was
* null or empty * null or empty
*/ */
@ -135,4 +137,58 @@ public final class StringSplitUtils {
return str.substring(pos + separator.length()); return str.substring(pos + separator.length());
} }
/**
* Splits a given string on the given separator character, skips the contents of quoted substrings
* when looking for separators.
* Introduced for use in DigestProcessingFilter (see SEC-506).
* <p/>
* This was copied and modified from commons-lang StringUtils
*/
public static String[] splitIgnoringQuotes(String str, char separatorChar) {
if (str == null) {
return null;
}
int len = str.length();
if (len == 0) {
return EMPTY_STRING_ARRAY;
}
List list = new ArrayList();
int i = 0;
int start = 0;
boolean match = false;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
while (i < len) {
if (str.charAt(i) == '"') {
i++;
break;
}
i++;
}
match = true;
continue;
}
if (str.charAt(i) == separatorChar) {
if (match) {
list.add(str.substring(start, i));
match = false;
}
start = ++i;
continue;
}
match = true;
i++;
}
if (match) {
list.add(str.substring(start, i));
}
return (String[]) list.toArray(new String[list.size()]);
}
} }

View File

@ -62,25 +62,28 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
private static final String NC = "00000002"; private static final String NC = "00000002";
private static final String CNONCE = "c822c727a648aba7"; private static final String CNONCE = "c822c727a648aba7";
private static final String REALM = "The Correct Realm Name"; private static final String REALM = "The Actual, Correct Realm Name";
private static final String KEY = "acegi"; private static final String KEY = "acegi";
private static final String QOP = "auth"; private static final String QOP = "auth";
private static final String USERNAME = "marissa"; private static final String USERNAME = "marissa,ok";
private static final String PASSWORD = "koala"; private static final String PASSWORD = "koala";
private static final String REQUEST_URI = "/some_file.html"; private static final String REQUEST_URI = "/some_file.html";
/** A standard valid nonce with a validity period of 60 seconds */ /**
* A standard valid nonce with a validity period of 60 seconds
*/
private static final String NONCE = generateNonce(60); private static final String NONCE = generateNonce(60);
//~ Instance fields ================================================================================================ //~ Instance fields ================================================================================================
// private ApplicationContext ctx; // private ApplicationContext ctx;
private DigestProcessingFilter filter; private DigestProcessingFilter filter;
private MockHttpServletRequest request; private MockHttpServletRequest request;
//~ Constructors =================================================================================================== //~ Constructors ===================================================================================================
public DigestProcessingFilterTests() {} public DigestProcessingFilterTests() {
}
public DigestProcessingFilterTests(String arg0) { public DigestProcessingFilterTests(String arg0) {
super(arg0); super(arg0);
@ -89,13 +92,13 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
private String createAuthorizationHeader(String username, String realm, String nonce, String uri, private String createAuthorizationHeader(String username, String realm, String nonce, String uri,
String responseDigest, String qop, String nc, String cnonce) { String responseDigest, String qop, String nc, String cnonce) {
return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + uri return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + uri
+ "\", response=\"" + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\""; + "\", response=\"" + responseDigest + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\"";
} }
private MockHttpServletResponse executeFilterInContainerSimulator(Filter filter, ServletRequest request, private MockHttpServletResponse executeFilterInContainerSimulator(Filter filter, ServletRequest request,
boolean expectChainToProceed) throws ServletException, IOException { boolean expectChainToProceed) throws ServletException, IOException {
filter.init(new MockFilterConfig()); filter.init(new MockFilterConfig());
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
@ -118,10 +121,6 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
return new String(Base64.encodeBase64(nonceValue.getBytes())); return new String(Base64.encodeBase64(nonceValue.getBytes()));
} }
public static void main(String[] args) {
junit.textui.TestRunner.run(DigestProcessingFilterTests.class);
}
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
@ -129,7 +128,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
// Create User Details Service // Create User Details Service
InMemoryDaoImpl dao = new InMemoryDaoImpl(); InMemoryDaoImpl dao = new InMemoryDaoImpl();
UserMapEditor editor = new UserMapEditor(); UserMapEditor editor = new UserMapEditor();
editor.setAsText("marissa=koala,ROLE_ONE,ROLE_TWO,enabled\r\n"); editor.setAsText("marissa,ok=koala,ROLE_ONE,ROLE_TWO,enabled\r\n");
dao.setUserMap((UserMap) editor.getValue()); dao.setUserMap((UserMap) editor.getValue());
DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint(); DigestProcessingFilterEntryPoint ep = new DigestProcessingFilterEntryPoint();
@ -150,7 +149,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testDoFilterWithNonHttpServletRequestDetected() public void testDoFilterWithNonHttpServletRequestDetected()
throws Exception { throws Exception {
DigestProcessingFilter filter = new DigestProcessingFilter(); DigestProcessingFilter filter = new DigestProcessingFilter();
try { try {
@ -162,7 +161,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testDoFilterWithNonHttpServletResponseDetected() public void testDoFilterWithNonHttpServletResponseDetected()
throws Exception { throws Exception {
DigestProcessingFilter filter = new DigestProcessingFilter(); DigestProcessingFilter filter = new DigestProcessingFilter();
try { try {
@ -174,13 +173,13 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testExpiredNonceReturnsForbiddenWithStaleHeader() public void testExpiredNonceReturnsForbiddenWithStaleHeader()
throws Exception { throws Exception {
String nonce = generateNonce(0); String nonce = generateNonce(0);
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, nonce, NC, CNONCE); REQUEST_URI, QOP, nonce, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
Thread.sleep(1000); // ensures token expired Thread.sleep(1000); // ensures token expired
@ -196,7 +195,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testFilterIgnoresRequestsContainingNoAuthorizationHeader() public void testFilterIgnoresRequestsContainingNoAuthorizationHeader()
throws Exception { throws Exception {
executeFilterInContainerSimulator(filter, request, true); executeFilterInContainerSimulator(filter, request, true);
assertNull(SecurityContextHolder.getContext().getAuthentication()); assertNull(SecurityContextHolder.getContext().getAuthentication());
@ -217,7 +216,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testInvalidDigestAuthorizationTokenGeneratesError() public void testInvalidDigestAuthorizationTokenGeneratesError()
throws Exception { throws Exception {
String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON"; String token = "NOT_A_VALID_TOKEN_AS_MISSING_COLON";
request.addHeader("Authorization", "Digest " + new String(Base64.encodeBase64(token.getBytes()))); request.addHeader("Authorization", "Digest " + new String(Base64.encodeBase64(token.getBytes())));
@ -238,14 +237,14 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testNonBase64EncodedNonceReturnsForbidden() public void testNonBase64EncodedNonceReturnsForbidden()
throws Exception { throws Exception {
String nonce = "NOT_BASE_64_ENCODED"; String nonce = "NOT_BASE_64_ENCODED";
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, nonce, NC, CNONCE); REQUEST_URI, QOP, nonce, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -254,13 +253,13 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testNonceWithIncorrectSignatureForNumericFieldReturnsForbidden() public void testNonceWithIncorrectSignatureForNumericFieldReturnsForbidden()
throws Exception { throws Exception {
String nonce = new String(Base64.encodeBase64("123456:incorrectStringPassword".getBytes())); String nonce = new String(Base64.encodeBase64("123456:incorrectStringPassword".getBytes()));
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, nonce, NC, CNONCE); REQUEST_URI, QOP, nonce, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -269,13 +268,13 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testNonceWithNonNumericFirstElementReturnsForbidden() public void testNonceWithNonNumericFirstElementReturnsForbidden()
throws Exception { throws Exception {
String nonce = new String(Base64.encodeBase64("hello:ignoredSecondElement".getBytes())); String nonce = new String(Base64.encodeBase64("hello:ignoredSecondElement".getBytes()));
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, nonce, NC, CNONCE); REQUEST_URI, QOP, nonce, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -284,13 +283,13 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testNonceWithoutTwoColonSeparatedElementsReturnsForbidden() public void testNonceWithoutTwoColonSeparatedElementsReturnsForbidden()
throws Exception { throws Exception {
String nonce = new String(Base64.encodeBase64("a base 64 string without a colon".getBytes())); String nonce = new String(Base64.encodeBase64("a base 64 string without a colon".getBytes()));
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, nonce, NC, CNONCE); REQUEST_URI, QOP, nonce, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, nonce, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -299,38 +298,38 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testNormalOperationWhenPasswordIsAlreadyEncoded() public void testNormalOperationWhenPasswordIsAlreadyEncoded()
throws Exception { throws Exception {
String encodedPassword = DigestProcessingFilter.encodePasswordInA1Format(USERNAME, REALM, PASSWORD); String encodedPassword = DigestProcessingFilter.encodePasswordInA1Format(USERNAME, REALM, PASSWORD);
String responseDigest = DigestProcessingFilter.generateDigest(true, USERNAME, REALM, encodedPassword, "GET", String responseDigest = DigestProcessingFilter.generateDigest(true, USERNAME, REALM, encodedPassword, "GET",
REQUEST_URI, QOP, NONCE, NC, CNONCE); REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
executeFilterInContainerSimulator(filter, request, true); executeFilterInContainerSimulator(filter, request, true);
assertNotNull(SecurityContextHolder.getContext().getAuthentication()); assertNotNull(SecurityContextHolder.getContext().getAuthentication());
assertEquals(USERNAME, assertEquals(USERNAME,
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()); ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername());
} }
public void testNormalOperationWhenPasswordNotAlreadyEncoded() public void testNormalOperationWhenPasswordNotAlreadyEncoded()
throws Exception { throws Exception {
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, NONCE, NC, CNONCE); REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
executeFilterInContainerSimulator(filter, request, true); executeFilterInContainerSimulator(filter, request, true);
assertNotNull(SecurityContextHolder.getContext().getAuthentication()); assertNotNull(SecurityContextHolder.getContext().getAuthentication());
assertEquals(USERNAME, assertEquals(USERNAME,
((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername()); ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername());
} }
public void testOtherAuthorizationSchemeIsIgnored() public void testOtherAuthorizationSchemeIsIgnored()
throws Exception { throws Exception {
request.addHeader("Authorization", "SOME_OTHER_AUTHENTICATION_SCHEME"); request.addHeader("Authorization", "SOME_OTHER_AUTHENTICATION_SCHEME");
executeFilterInContainerSimulator(filter, request, true); executeFilterInContainerSimulator(filter, request, true);
@ -339,7 +338,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testStartupDetectsMissingAuthenticationEntryPoint() public void testStartupDetectsMissingAuthenticationEntryPoint()
throws Exception { throws Exception {
try { try {
DigestProcessingFilter filter = new DigestProcessingFilter(); DigestProcessingFilter filter = new DigestProcessingFilter();
filter.setUserDetailsService(new InMemoryDaoImpl()); filter.setUserDetailsService(new InMemoryDaoImpl());
@ -351,7 +350,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testStartupDetectsMissingUserDetailsService() public void testStartupDetectsMissingUserDetailsService()
throws Exception { throws Exception {
try { try {
DigestProcessingFilter filter = new DigestProcessingFilter(); DigestProcessingFilter filter = new DigestProcessingFilter();
filter.setAuthenticationEntryPoint(new DigestProcessingFilterEntryPoint()); filter.setAuthenticationEntryPoint(new DigestProcessingFilterEntryPoint());
@ -363,12 +362,12 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testSuccessLoginThenFailureLoginResultsInSessionLosingToken() public void testSuccessLoginThenFailureLoginResultsInSessionLosingToken()
throws Exception { throws Exception {
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, NONCE, NC, CNONCE); REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
executeFilterInContainerSimulator(filter, request, true); executeFilterInContainerSimulator(filter, request, true);
@ -380,7 +379,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
request = new MockHttpServletRequest(); request = new MockHttpServletRequest();
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -390,14 +389,14 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
} }
public void testWrongCnonceBasedOnDigestReturnsForbidden() public void testWrongCnonceBasedOnDigestReturnsForbidden()
throws Exception { throws Exception {
String cnonce = "NOT_SAME_AS_USED_FOR_DIGEST_COMPUTATION"; String cnonce = "NOT_SAME_AS_USED_FOR_DIGEST_COMPUTATION";
String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET", String responseDigest = DigestProcessingFilter.generateDigest(false, USERNAME, REALM, PASSWORD, "GET",
REQUEST_URI, QOP, NONCE, NC, "DIFFERENT_CNONCE"); REQUEST_URI, QOP, NONCE, NC, "DIFFERENT_CNONCE");
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, cnonce)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, cnonce));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -411,7 +410,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
REQUEST_URI, QOP, NONCE, NC, CNONCE); REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -425,7 +424,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
REQUEST_URI, QOP, NONCE, NC, CNONCE); REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, realm, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, realm, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);
@ -438,7 +437,7 @@ public class DigestProcessingFilterTests extends MockObjectTestCase {
"GET", REQUEST_URI, QOP, NONCE, NC, CNONCE); "GET", REQUEST_URI, QOP, NONCE, NC, CNONCE);
request.addHeader("Authorization", request.addHeader("Authorization",
createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE)); createAuthorizationHeader(USERNAME, REALM, NONCE, REQUEST_URI, responseDigest, QOP, NC, CNONCE));
MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false); MockHttpServletResponse response = executeFilterInContainerSimulator(filter, request, false);

View File

@ -32,6 +32,7 @@ public class StringSplitUtilsTests extends TestCase {
//~ Constructors =================================================================================================== //~ Constructors ===================================================================================================
// =========================================================== // ===========================================================
public StringSplitUtilsTests() { public StringSplitUtilsTests() {
super(); super();
} }
@ -43,6 +44,7 @@ public class StringSplitUtilsTests extends TestCase {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
// ================================================================ // ================================================================
public static void main(String[] args) { public static void main(String[] args) {
junit.textui.TestRunner.run(StringSplitUtilsTests.class); junit.textui.TestRunner.run(StringSplitUtilsTests.class);
} }
@ -57,7 +59,7 @@ public class StringSplitUtilsTests extends TestCase {
assertEquals("Contacts Realm", headerMap.get("realm")); assertEquals("Contacts Realm", headerMap.get("realm"));
assertEquals("MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==", headerMap.get("nonce")); assertEquals("MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==", headerMap.get("nonce"));
assertEquals("/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4", assertEquals("/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4",
headerMap.get("uri")); headerMap.get("uri"));
assertEquals("38644211cf9ac3da63ab639807e2baff", headerMap.get("response")); assertEquals("38644211cf9ac3da63ab639807e2baff", headerMap.get("response"));
assertEquals("auth", headerMap.get("qop")); assertEquals("auth", headerMap.get("qop"));
assertEquals("00000004", headerMap.get("nc")); assertEquals("00000004", headerMap.get("nc"));
@ -74,7 +76,7 @@ public class StringSplitUtilsTests extends TestCase {
assertEquals("\"Contacts Realm\"", headerMap.get("realm")); assertEquals("\"Contacts Realm\"", headerMap.get("realm"));
assertEquals("\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\"", headerMap.get("nonce")); assertEquals("\"MTEwOTAyMzU1MTQ4NDo1YzY3OWViYWM5NDNmZWUwM2UwY2NmMDBiNDQzMTQ0OQ==\"", headerMap.get("nonce"));
assertEquals("\"/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\"", assertEquals("\"/acegi-security-sample-contacts-filter/secure/adminPermission.htm?contactId=4\"",
headerMap.get("uri")); headerMap.get("uri"));
assertEquals("\"38644211cf9ac3da63ab639807e2baff\"", headerMap.get("response")); assertEquals("\"38644211cf9ac3da63ab639807e2baff\"", headerMap.get("response"));
assertEquals("auth", headerMap.get("qop")); assertEquals("auth", headerMap.get("qop"));
assertEquals("00000004", headerMap.get("nc")); assertEquals("00000004", headerMap.get("nc"));
@ -84,7 +86,7 @@ public class StringSplitUtilsTests extends TestCase {
public void testSplitEachArrayElementAndCreateMapReturnsNullIfArrayEmptyOrNull() { public void testSplitEachArrayElementAndCreateMapReturnsNullIfArrayEmptyOrNull() {
assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(null, "=", "\"")); assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(null, "=", "\""));
assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(new String[] {}, "=", "\"")); assertNull(StringSplitUtils.splitEachArrayElementAndCreateMap(new String[]{}, "=", "\""));
} }
public void testSplitNormalOperation() { public void testSplitNormalOperation() {
@ -137,4 +139,14 @@ public class StringSplitUtilsTests extends TestCase {
// only guarantees to split at FIRST delimiter, not EACH delimiter // only guarantees to split at FIRST delimiter, not EACH delimiter
assertEquals(2, StringSplitUtils.split("18|marissa|foo|bar", "|").length); assertEquals(2, StringSplitUtils.split("18|marissa|foo|bar", "|").length);
} }
public void testAuthorizationHeaderWithCommasIsSplitCorrectly() {
String header = "Digest username=\"hamilton,bob\", realm=\"bobs,ok,realm\", nonce=\"the,nonce\", " +
"uri=\"the,Uri\", response=\"the,response,Digest\", qop=theqop, nc=thenc, cnonce=\"the,cnonce\"";
String[] parts = StringSplitUtils.splitIgnoringQuotes(header, ',');
assertEquals(8, parts.length);
}
} }