1
0
mirror of synced 2026-08-05 17:57:15 +00:00

Added Digest Authentication support (RFC 2617 and RFC 2069).

This commit is contained in:
Ben Alex
2005-02-22 06:14:44 +00:00
parent cbf413afcd
commit a3818184f4
14 changed files with 2189 additions and 38 deletions
@@ -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 <code>HttpSession</code>.
* into the <code>ContextHolder</code>.
*
* <P>
* For a detailed background on what this filter is designed to process, refer
@@ -75,9 +74,7 @@ import javax.servlet.http.HttpServletResponse;
*
* <P>
* If authentication is successful, the resulting {@link Authentication} object
* will be placed into the <code>HttpSession</code> with the attribute defined
* by {@link
* HttpSessionContextIntegrationFilter#ACEGI_SECURITY_AUTHENTICATION_KEY}.
* will be placed into the <code>ContextHolder</code>.
* </p>
*
* <p>
@@ -87,6 +84,15 @@ import javax.servlet.http.HttpServletResponse;
* </p>
*
* <P>
* 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}.
* </p>
*
* <P>
* <B>Do not use this class directly.</B> Instead configure
* <code>web.xml</code> to use the {@link
* net.sf.acegisecurity.util.FilterToBeanProxy}.
@@ -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 <code>ContextHolder</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
* superseeded RFC 2069, although this 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 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. All session management information is
* stored in the "nonce" that is sent to the client by the {@link
* net.sf.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint}.
* </p>
*
* <P>
* If authentication is successful, the resulting {@link Authentication} object
* will be placed into the <code>ContextHolder</code>.
* </p>
*
* <p>
* 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.
* </p>
*
* <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
* 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
* net.sf.acegisecurity.util.FilterToBeanProxy}.
* </p>
*
* @author Ben Alex
* @version $Id$
*/
public class DigestProcessingFilter implements Filter, InitializingBean {
//~ Static fields/initializers =============================================
private static final Log logger = LogFactory.getLog(DigestProcessingFilter.class);
//~ Instance fields ========================================================
private AuthenticationDao authenticationDao;
private DigestProcessingFilterEntryPoint authenticationEntryPoint;
private UserCache userCache = new NullUserCache();
//~ Methods ================================================================
public void setAuthenticationDao(AuthenticationDao authenticationDao) {
this.authenticationDao = authenticationDao;
}
public AuthenticationDao getAuthenticationDao() {
return authenticationDao;
}
public void setAuthenticationEntryPoint(
DigestProcessingFilterEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
public DigestProcessingFilterEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}
public void afterPropertiesSet() throws Exception {
if (this.authenticationDao == null) {
throw new IllegalArgumentException(
"An AuthenticationDao is required");
}
if (this.authenticationEntryPoint == null) {
throw new IllegalArgumentException(
"A DigestProcessingFilterEntryPoint is required");
}
}
public void destroy() {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (!(request instanceof HttpServletRequest)) {
throw new ServletException("Can only process HttpServletRequest");
}
if (!(response instanceof HttpServletResponse)) {
throw new ServletException("Can only process HttpServletResponse");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
String header = httpRequest.getHeader("Authorization");
if (logger.isDebugEnabled()) {
logger.debug("Authorization header received from user agent: "
+ header);
}
if ((header != null) && header.startsWith("Digest ")) {
String section212response = header.substring(7);
String[] headerEntries = StringUtils
.commaDelimitedListToStringArray(section212response);
Map headerMap = StringSplitUtils.splitEachArrayElementAndCreateMap(headerEntries,
"=", "\"");
String username = (String) headerMap.get("username");
String realm = (String) headerMap.get("realm");
String nonce = (String) headerMap.get("nonce");
String uri = (String) headerMap.get("uri");
String responseDigest = (String) headerMap.get("response");
String qop = (String) headerMap.get("qop"); // RFC 2617 extension
String nc = (String) headerMap.get("nc"); // RFC 2617 extension
String cnonce = (String) headerMap.get("cnonce"); // RFC 2617 extension
// Check all required parameters were supplied (ie RFC 2069)
if ((username == null) || (realm == null) || (nonce == null)
|| (uri == null) || (response == null)) {
if (logger.isDebugEnabled()) {
logger.debug("extracted username: '" + username
+ "'; realm: '" + username + "'; nonce: '" + username
+ "'; uri: '" + username + "'; response: '" + username
+ "'");
}
fail(request, response,
new BadCredentialsException(
"Missing mandatory digest value; received header '"
+ section212response + "'"));
return;
}
// Check all required parameters for an "auth" qop were supplied (ie RFC 2617)
if ("auth".equals(qop)) {
if ((nc == null) || (cnonce == null)) {
if (logger.isDebugEnabled()) {
logger.debug("extracted nc: '" + nc + "'; cnonce: '"
+ cnonce + "'");
}
fail(request, response,
new BadCredentialsException(
"Missing mandatory digest value for 'auth' QOP; received header '"
+ section212response + "'"));
return;
}
}
// Check realm name equals what we expected
if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) {
fail(request, response,
new BadCredentialsException("Response realm name '" + realm
+ "' does not match system realm name of '"
+ this.getAuthenticationEntryPoint().getRealmName()
+ "'"));
return;
}
// Check nonce was a Base64 encoded (as sent by DigestProcessingFilterEntryPoint)
if (!Base64.isArrayByteBase64(nonce.getBytes())) {
fail(request, response,
new BadCredentialsException(
"None is not encoded in Base64; received nonce: '"
+ nonce + "'"));
return;
}
// Decode nonce from Base64
// format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
String nonceAsPlainText = new String(Base64.decodeBase64(
nonce.getBytes()));
String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText,
":");
if (nonceTokens.length != 2) {
fail(request, response,
new BadCredentialsException(
"Nonce should have yielded two tokens but was: '"
+ nonceAsPlainText + "'"));
return;
}
// Extract expiry time from nonce
long nonceExpiryTime;
try {
nonceExpiryTime = new Long(nonceTokens[0]).longValue();
} catch (NumberFormatException nfe) {
fail(request, response,
new BadCredentialsException(
"Nonce token should have yielded a numeric first token, but was: '"
+ nonceAsPlainText + "'"));
return;
}
// Check signature of nonce matches this expiry time
String expectedNonceSignature = DigestUtils.md5Hex(nonceExpiryTime
+ ":" + this.getAuthenticationEntryPoint().getKey());
if (!expectedNonceSignature.equals(nonceTokens[1])) {
fail(request, response,
new BadCredentialsException("Nonce token compromised: '"
+ nonceAsPlainText + "'"));
return;
}
// Lookup password for presented username
// NB: DAO-provided password MUST be clear text - not encoded/salted
boolean loadedFromDao = false;
UserDetails user = userCache.getUserFromCache(username);
if (user == null) {
loadedFromDao = true;
try {
user = authenticationDao.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) {
fail(request, response,
new BadCredentialsException("Username '" + username
+ "' not known"));
return;
}
if (user == null) {
throw new AuthenticationServiceException(
"AuthenticationDao returned null, which is an interface contract violation");
}
userCache.putUserInCache(user);
}
// Compute the expected response-digest (will be in hex form)
String serverDigestMd5;
// Don't catch IllegalArgumentException (already checked validity)
serverDigestMd5 = generateDigest(username, realm,
user.getPassword(),
((HttpServletRequest) request).getMethod(), uri, qop,
nonce, nc, cnonce);
// If digest is incorrect, try refreshing from backend and recomputing
if (!serverDigestMd5.equals(responseDigest) && !loadedFromDao) {
if (logger.isDebugEnabled()) {
logger.debug(
"Digest comparison failure; trying to refresh user from DAO in case password had changed");
}
try {
user = authenticationDao.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) {
// Would very rarely happen, as user existed earlier
fail(request, response,
new BadCredentialsException("Username '" + username
+ "' not known"));
}
userCache.putUserInCache(user);
// Don't catch IllegalArgumentException (already checked validity)
serverDigestMd5 = generateDigest(username, realm,
user.getPassword(),
((HttpServletRequest) request).getMethod(), uri, qop,
nonce, nc, cnonce);
}
// If digest is still incorrect, definitely reject authentication attempt
if (!serverDigestMd5.equals(responseDigest)) {
if (logger.isDebugEnabled()) {
logger.debug("Expected response: '" + serverDigestMd5
+ "' but received: '" + responseDigest
+ "'; is AuthenticationDao returning clear text passwords?");
}
fail(request, response,
new BadCredentialsException("Incorrect response"));
return;
}
// To get this far, the digest must have been valid
// Check the nonce has not expired
// We do this last so we can direct the user agent its nonce is stale
// but the request was otherwise appearing to be valid
if (nonceExpiryTime < System.currentTimeMillis()) {
fail(request, response,
new NonceExpiredException("Nonce has expired/timed out"));
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success for user: '" + username
+ "' with response: '" + responseDigest + "'");
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user,
user.getPassword());
authRequest.setDetails(new WebAuthenticationDetails(httpRequest));
SecureContext sc = SecureContextUtils.getSecureContext();
sc.setAuthentication(authRequest);
ContextHolder.setContext(sc);
}
chain.doFilter(request, response);
}
/**
* Computes the <code>response</code> portion of a Digest authentication
* header. Both the server and user agent should compute the
* <code>response</code> 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);
}
}
@@ -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 <code>SecurityEnforcementFilter</code> to commence
* authentication via the {@link DigestProcessingFilter}.
*
* <p>
* 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 <code>stale=true</code> 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).
* </p>
*
* @author Ben Alex
* @version $Id$
*/
public class DigestProcessingFilterEntryPoint
implements AuthenticationEntryPoint, InitializingBean {
//~ Static fields/initializers =============================================
private static final Log logger = LogFactory.getLog(DigestProcessingFilterEntryPoint.class);
//~ Instance fields ========================================================
private String key;
private String realmName;
private int nonceValiditySeconds = 300;
//~ Methods ================================================================
public void setKey(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public void setNonceValiditySeconds(int nonceValiditySeconds) {
this.nonceValiditySeconds = nonceValiditySeconds;
}
public int getNonceValiditySeconds() {
return nonceValiditySeconds;
}
public void setRealmName(String realmName) {
this.realmName = realmName;
}
public String getRealmName() {
return realmName;
}
public void afterPropertiesSet() throws Exception {
if ((realmName == null) || "".equals(realmName)) {
throw new IllegalArgumentException("realmName must be specified");
}
if ((key == null) || "".equals(key)) {
throw new IllegalArgumentException("key must be specified");
}
}
public void commence(ServletRequest request, ServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// compute a nonce (do not use remote IP address due to proxy farms)
// format of nonce is:
// base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
long expiryTime = System.currentTimeMillis()
+ (nonceValiditySeconds * 1000);
String signatureValue = new String(DigestUtils.md5Hex(expiryTime + ":"
+ key));
String nonceValue = expiryTime + ":" + signatureValue;
String nonceValueBase64 = new String(Base64.encodeBase64(
nonceValue.getBytes()));
// qop is quality of protection, as defined by RFC 2617.
// we do not use opaque due to IE violation of RFC 2617 in not
// representing opaque on subsequent requests in same session.
String authenticateHeader = "Digest realm=\"" + realmName + "\", "
+ "qop=\"auth\", nonce=\"" + nonceValueBase64 + "\"";
if (authException instanceof NonceExpiredException) {
authenticateHeader = authenticateHeader + ", stale=\"true\"";
}
if (logger.isDebugEnabled()) {
logger.debug("WWW-Authenticate header sent to user agent: "
+ authenticateHeader);
}
httpResponse.addHeader("WWW-Authenticate", authenticateHeader);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
authException.getMessage());
}
}
@@ -0,0 +1,51 @@
/* 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;
/**
* Thrown if an authentication request is rejected because the digest nonce has
* expired.
*
* @author Ben Alex
* @version $Id$
*/
public class NonceExpiredException extends AuthenticationException {
//~ Constructors ===========================================================
/**
* Constructs a <code>NonceExpiredException</code> with the specified
* message.
*
* @param msg the detail message
*/
public NonceExpiredException(String msg) {
super(msg);
}
/**
* Constructs a <code>NonceExpiredException</code> 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);
}
}
@@ -0,0 +1,5 @@
<html>
<body>
Authenticates HTTP Digest authentication requests.
</body>
</html>
@@ -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 <code>String</code> manipulation methods.
*
* @author Ben Alex
* @version $Id$
*/
public class StringSplitUtils {
//~ Methods ================================================================
/**
* Splits a <code>String</code> at the first instance of the delimiter.
*
* <p>
* Does not include the delimiter in the response.
* </p>
*
* @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 of <code>String</code>s, and for each element removes any
* instances of <code>removeCharacter</code>, and splits the element based
* on the <code>delimiter</code>. A <code>Map</code> is 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>
*
* @param array the array to process
* @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 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 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;
}
}
@@ -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) {
@@ -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]);
}
}
@@ -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");
}
}
}
}
@@ -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);
}
}
}
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<!--
* Copyright 2004 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
* $Id$
-->
<beans>
<!-- Data access object which stores authentication information -->
<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR
dianne=emu,ROLE_TELLER
scott=wombat,ROLE_TELLER
peter=opal,disabled,ROLE_TELLER
</value>
</property>
</bean>
<!-- Authentication provider that queries our data access object -->
<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property>
</bean>
<!-- The authentication manager that iterates through our only authentication provider -->
<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
</list>
</property>
</bean>
<bean id="digestProcessingFilter" class="net.sf.acegisecurity.ui.digestauth.DigestProcessingFilter">
<property name="authenticationDao"><ref local="inMemoryDaoImpl"/></property>
<property name="authenticationEntryPoint"><ref local="digestProcessingFilterEntryPoint"/></property>
</bean>
<bean id="digestProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint">
<property name="realmName"><value>Contacts Realm via Digest Authentication</value></property>
<property name="key"><value>acegi</value></property>
<property name="nonceValiditySeconds"><value>1000</value></property>
</bean>
</beans>