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

View File

@ -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}.

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
<html>
<body>
Authenticates HTTP Digest authentication requests.
</body>
</html>

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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]);
}
}

View File

@ -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");
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -2406,34 +2406,21 @@ public boolean supports(Class clazz);</programlisting></para>
<para>The Acegi Security System for Spring provides a
<literal>BasicProcessingFilter</literal> which is capable of
processing authentication credentials presented in HTTP headers. This
can be used for authenticating calls made by Spring remoting protocols
(such as Hessian and Burlap), as well as normal user agents (such as
Internet Explorer and Navigator). The standard governing HTTP Basic
Authentication is defined by RFC 1945, Section 11, and the
<literal>BasicProcessingFilter</literal> conforms with this
RFC.</para>
processing basic authentication credentials presented in HTTP headers.
This can be used for authenticating calls made by Spring remoting
protocols (such as Hessian and Burlap), as well as normal user agents
(such as Internet Explorer and Navigator). The standard governing HTTP
Basic Authentication is defined by RFC 1945, Section 11, and the
<literal>BasicProcessingFilter</literal> conforms with this RFC. Basic
Authentication is an attractive approach to authentication, because it
is very widely deployed in user agents and implementation is extremely
simple (it's just a Base64 encoding of the username:password,
specified in a HTTP header).</para>
<para>To implement HTTP Basic Authentication, it is necessary to add
the following filter to <literal>web.xml</literal>:</para>
<para><programlisting>&lt;filter&gt;
&lt;filter-name&gt;Acegi HTTP BASIC Authorization Filter&lt;/filter-name&gt;
&lt;filter-class&gt;net.sf.acegisecurity.util.FilterToBeanProxy&lt;/filter-class&gt;
&lt;init-param&gt;
&lt;param-name&gt;targetClass&lt;/param-name&gt;
&lt;param-value&gt;net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter&lt;/param-value&gt;
&lt;/init-param&gt;
&lt;/filter&gt;
&lt;filter-mapping&gt;
&lt;filter-name&gt;Acegi HTTP BASIC Authorization Filter&lt;/filter-name&gt;
&lt;url-pattern&gt;/*&lt;/url-pattern&gt;
&lt;/filter-mapping&gt;</programlisting></para>
<para>For a discussion of <literal>FilterToBeanProxy</literal>, please
refer to the Filters section. The application context will need to
define the <literal>BasicProcessingFilter</literal> and its required
<para>To implement HTTP Basic Authentication, it is necessary to
define <literal>BasicProcessingFilter</literal> in the fitler chain.
The application context will need to define the
<literal>BasicProcessingFilter</literal> and its required
collaborator:</para>
<para><programlisting>&lt;bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"&gt;
@ -2461,12 +2448,136 @@ public boolean supports(Class clazz);</programlisting></para>
only time the filter chain will be interrupted is if authentication
fails and the <literal>AuthenticationEntryPoint</literal> is called,
as discussed in the previous paragraph.</para>
</sect2>
<para>HTTP Basic Authentication is recommended to be used instead of
Container Adapters. It can be used in conjunction with HTTP Form
Authentication, as demonstrated in the Contacts sample application.
You can also use it instead of HTTP Form Authentication if you
wish.</para>
<sect2 id="security-ui-http-digest">
<title>HTTP Digest Authentication</title>
<para>The Acegi Security System for Spring provides a
<literal>DigestProcessingFilter</literal> which is capable of
processing digest authentication credentials presented in HTTP
headers. Digest Authentication attempts to solve many of the
weakenesses of Basic authentication, specifically by ensuring
credentials are never sent in clear text across the wire. Many user
agents support Digest Authentication, including FireFox and Internet
Explorer. The standard governing HTTP Digest Authentication is defined
by RFC 2617, which updates an earlier version of the Digest
Authentication standard prescribed by RFC 2069. Most user agents
implement RFC 2617. The Acegi Security
<literal>DigestProcessingFilter</literal> is compatible with the
"<literal>auth</literal>" quality of protection
(<literal>qop</literal>) prescribed by RFC 2617, which also provides
backward compatibility with RFC 2069. Digest Authentication is a
highly attractive option if you need to use unencrypted HTTP (ie no
TLS/HTTPS) and wish to maximise security of the authentication
process. Indeed Digest Authentication is a mandatory requirement for
the WebDAV protocol, as noted by RFC 2518 Section 17.1, so we should
expect to see it increasingly deployed and replacing Basic
Authentication.</para>
<para>Digest Authentication is definitely the most secure choice
between Form Authentication, Basic Authentication and Digest
Authentication, although extra security also means more complex user
agent implementations. Central to Digest Authentication is a "nonce".
This is a value the server generates. Acegi Security's nonce adopts
the following format:</para>
<para><programlisting>base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime: The date and time when the nonce expires, expressed in milliseconds
key: A private key to prevent modification of the nounce token
</programlisting></para>
<para>The <literal>DigestProcessingFilterEntryPoint</literal> has a
property specifying the <literal>key</literal> used for generating the
nonce tokens, along with a <literal>nonceValiditySeconds</literal>
property for determining the expiration time (default 300, which
equals five minutes). Whilstever the nonce is valid, the digest is
computed by concatenating various strings including the username,
password, nonce, URI being requested, a client-generated nonce (merely
a random value which the user agent generates each request), the realm
name etc, then performing an MD5 hash. Both the server and user agent
perform this digest computation, resulting in different hash codes if
they disagree on an included value (eg password). In the Acegi
Security implementation, if the server-generated nonce has merely
expired (but the digest was otherwise valid), the
<literal>DigestProcessingFilterEntryPoint</literal> will send a
<literal>"stale=true"</literal> header. This tells the user agent
there is no need to disturb the user (as the password and username etc
is correct), but simply to try again using a new nonce.</para>
<para>An appropriate value for
<literal>DigestProcessingFilterEntryPoint</literal>'s
<literal>nonceValiditySeconds</literal> parameter will depend on your
application. Extremely secure applications should note that an
intercepted authentication header can be used to impersonate the
principal until the <literal>expirationTime</literal> contained in the
nonce is reached. This is the key principle when selecting an
appropriate setting, but it would be unusual for immensly secure
applications to not be running over TLS/HTTPS in the first
instance.</para>
<para>Because of the more complex implementation of Digest
Authentication, there are often user agent issues. For example,
Internet Explorer fails to present an "<literal>opaque</literal>"
token on subsequent requests in the same session. The Acegi Security
filters therefore encapsulate all state information into the
"<literal>nonce</literal>" token instead. In our testing, the Acegi
Security implementation works reliably with FireFox and Internet
Explorer, correctly handling nonce timeouts etc.</para>
<para>Now that we've reviewed the theory, let's see how to use it. To
implement HTTP Digest Authentication, it is necessary to define
<literal>DigestProcessingFilter</literal> in the fitler chain. The
application context will need to define the
<literal>DigestProcessingFilter</literal> and its required
collaborators:</para>
<para><programlisting>&lt;bean id="digestProcessingFilter" class="net.sf.acegisecurity.ui.digestauth.DigestProcessingFilter"&gt;
&lt;property name="authenticationDao"&gt;&lt;ref local="jdbcDaoImpl"/&gt;&lt;/property&gt;
&lt;property name="authenticationEntryPoint"&gt;&lt;ref local="digestProcessingFilterEntryPoint"/&gt;&lt;/property&gt;
&lt;property name="userCache"&gt;&lt;ref local="userCache"/&gt;&lt;/property&gt;
&lt;/bean&gt;
&lt;bean id="digestProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint"&gt;
&lt;property name="realmName"&gt;&lt;value&gt;Contacts Realm via Digest Authentication&lt;/value&gt;&lt;/property&gt;
&lt;property name="key"&gt;&lt;value&gt;acegi&lt;/value&gt;&lt;/property&gt;
&lt;property name="nonceValiditySeconds"&gt;&lt;value&gt;10&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;</programlisting></para>
<para>The configured <literal>AuthenticationDao</literal> is needed
because <literal>DigestProcessingFilter</literal> must have direct
access to the clear text password of a user. Digest Authentication
will NOT work if you are using encoded passwords ni your DAO. The DAO
collaborator, along with the <literal>UserCache</literal>, are
typically shared directly with a
<literal>DaoAuthenticationProvider</literal>. The
<literal>authenticationEntryPoint</literal> property must be
<literal>DigestProcessingFilterEntryPoint</literal>, so that
<literal>DigestProcessingFilter</literal> can obtain the correct
<literal>realmName</literal> and <literal>key</literal> for digest
calculations. </para>
<para>Like <literal>BasicAuthenticationFilter</literal>, if
authentication is successful an <literal>Authentication</literal>
request token will be placed into the
<literal>ContextHolder</literal>. If the authentication event was
successful, or authentication was not attempted because the HTTP
header did not contain a Digest Authentication request, the filter
chain will continue as normal. The only time the filter chain will be
interrupted is if authentication fails and the
<literal>AuthenticationEntryPoint</literal> is called, as discussed in
the previous paragraph.</para>
<para>Digest Authentication's RFC offers a range of additional
features to further increase security. For example, the nonce can be
changed on every request. Despite this, the Acegi Security
implementation was designed to minimise the complexity of the
implementation (and the doubtless user agent incompatibilities that
would emerge), and avoid needing to store server-side state. You are
invited to review RFC 2617 if you wish to explore these features in
more detail. As far as we are aware, the Acegi Security implementation
does comply with the minimum standards of this RFC.</para>
</sect2>
<sect2 id="security-ui-well-known">

View File

@ -26,6 +26,7 @@
</properties>
<body>
<release version="0.8.0" date="CVS">
<action dev="benalex" type="add">Added Digest Authentication support (RFC 2617 and RFC 2069)</action>
<action dev="benalex" type="update">Made ConfigAttributeDefinition and ConfigAttribute Serializable</action>
<action dev="benalex" type="update">User now accepts blank passwords (null passwords still rejected)</action>
<action dev="benalex" type="update">FilterToBeanProxy now searches hierarchical bean factories</action>

View File

@ -82,6 +82,11 @@
protocols or those web applications that prefer a simple browser pop-up
(rather than a form login), Acegi Security can directly process HTTP
BASIC authentication requests as per RFC 1945.<BR><BR>
<LI><B>Supports HTTP Digest authentication:</B> For greater security than
offered by BASIC authentcation, Acegi Security also supports Digest Authentication
(which never sends the user's password across the wire). Digest Authentication
is widely supported by modern browsers. Acegi Security's implementation complies
with both RFC 2617 and RFC 2069.<BR><BR>
<LI><B>Convenient security taglib:</B> Your JSP files can use our taglib
to ensure that protected content like links and messages are only
displayed to users holding the appropriate granted authorities. The taglib