mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-26 22:02:41 +00:00
Added Digest Authentication support (RFC 2617 and RFC 2069).
This commit is contained in:
parent
cbf413afcd
commit
a3818184f4
@ -19,7 +19,6 @@ import net.sf.acegisecurity.Authentication;
|
|||||||
import net.sf.acegisecurity.AuthenticationException;
|
import net.sf.acegisecurity.AuthenticationException;
|
||||||
import net.sf.acegisecurity.AuthenticationManager;
|
import net.sf.acegisecurity.AuthenticationManager;
|
||||||
import net.sf.acegisecurity.context.ContextHolder;
|
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.SecureContext;
|
||||||
import net.sf.acegisecurity.context.security.SecureContextUtils;
|
import net.sf.acegisecurity.context.security.SecureContextUtils;
|
||||||
import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
|
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
|
* Processes a HTTP request's BASIC authorization headers, putting the result
|
||||||
* into the <code>HttpSession</code>.
|
* into the <code>ContextHolder</code>.
|
||||||
*
|
*
|
||||||
* <P>
|
* <P>
|
||||||
* For a detailed background on what this filter is designed to process, refer
|
* For a detailed background on what this filter is designed to process, refer
|
||||||
@ -75,9 +74,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
*
|
*
|
||||||
* <P>
|
* <P>
|
||||||
* If authentication is successful, the resulting {@link Authentication} object
|
* If authentication is successful, the resulting {@link Authentication} object
|
||||||
* will be placed into the <code>HttpSession</code> with the attribute defined
|
* will be placed into the <code>ContextHolder</code>.
|
||||||
* by {@link
|
|
||||||
* HttpSessionContextIntegrationFilter#ACEGI_SECURITY_AUTHENTICATION_KEY}.
|
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>
|
||||||
@ -87,6 +84,15 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* <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
|
* <B>Do not use this class directly.</B> Instead configure
|
||||||
* <code>web.xml</code> to use the {@link
|
* <code>web.xml</code> to use the {@link
|
||||||
* net.sf.acegisecurity.util.FilterToBeanProxy}.
|
* 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>
|
123
core/src/main/java/org/acegisecurity/util/StringSplitUtils.java
Normal file
123
core/src/main/java/org/acegisecurity/util/StringSplitUtils.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -171,7 +171,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getMethod() {
|
public String getMethod() {
|
||||||
throw new UnsupportedOperationException("mock method not implemented");
|
return "GET";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setParameter(String arg0, String value) {
|
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>
|
@ -2406,34 +2406,21 @@ public boolean supports(Class clazz);</programlisting></para>
|
|||||||
|
|
||||||
<para>The Acegi Security System for Spring provides a
|
<para>The Acegi Security System for Spring provides a
|
||||||
<literal>BasicProcessingFilter</literal> which is capable of
|
<literal>BasicProcessingFilter</literal> which is capable of
|
||||||
processing authentication credentials presented in HTTP headers. This
|
processing basic authentication credentials presented in HTTP headers.
|
||||||
can be used for authenticating calls made by Spring remoting protocols
|
This can be used for authenticating calls made by Spring remoting
|
||||||
(such as Hessian and Burlap), as well as normal user agents (such as
|
protocols (such as Hessian and Burlap), as well as normal user agents
|
||||||
Internet Explorer and Navigator). The standard governing HTTP Basic
|
(such as Internet Explorer and Navigator). The standard governing HTTP
|
||||||
Authentication is defined by RFC 1945, Section 11, and the
|
Basic Authentication is defined by RFC 1945, Section 11, and the
|
||||||
<literal>BasicProcessingFilter</literal> conforms with this
|
<literal>BasicProcessingFilter</literal> conforms with this RFC. Basic
|
||||||
RFC.</para>
|
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
|
<para>To implement HTTP Basic Authentication, it is necessary to
|
||||||
the following filter to <literal>web.xml</literal>:</para>
|
define <literal>BasicProcessingFilter</literal> in the fitler chain.
|
||||||
|
The application context will need to define the
|
||||||
<para><programlisting><filter>
|
<literal>BasicProcessingFilter</literal> and its required
|
||||||
<filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
|
|
||||||
<filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
|
|
||||||
<init-param>
|
|
||||||
<param-name>targetClass</param-name>
|
|
||||||
<param-value>net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter</param-value>
|
|
||||||
</init-param>
|
|
||||||
</filter>
|
|
||||||
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
|
|
||||||
<url-pattern>/*</url-pattern>
|
|
||||||
</filter-mapping></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
|
|
||||||
collaborator:</para>
|
collaborator:</para>
|
||||||
|
|
||||||
<para><programlisting><bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
|
<para><programlisting><bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
|
||||||
@ -2461,12 +2448,136 @@ public boolean supports(Class clazz);</programlisting></para>
|
|||||||
only time the filter chain will be interrupted is if authentication
|
only time the filter chain will be interrupted is if authentication
|
||||||
fails and the <literal>AuthenticationEntryPoint</literal> is called,
|
fails and the <literal>AuthenticationEntryPoint</literal> is called,
|
||||||
as discussed in the previous paragraph.</para>
|
as discussed in the previous paragraph.</para>
|
||||||
|
</sect2>
|
||||||
|
|
||||||
<para>HTTP Basic Authentication is recommended to be used instead of
|
<sect2 id="security-ui-http-digest">
|
||||||
Container Adapters. It can be used in conjunction with HTTP Form
|
<title>HTTP Digest Authentication</title>
|
||||||
Authentication, as demonstrated in the Contacts sample application.
|
|
||||||
You can also use it instead of HTTP Form Authentication if you
|
<para>The Acegi Security System for Spring provides a
|
||||||
wish.</para>
|
<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><bean id="digestProcessingFilter" class="net.sf.acegisecurity.ui.digestauth.DigestProcessingFilter">
|
||||||
|
<property name="authenticationDao"><ref local="jdbcDaoImpl"/></property>
|
||||||
|
<property name="authenticationEntryPoint"><ref local="digestProcessingFilterEntryPoint"/></property>
|
||||||
|
<property name="userCache"><ref local="userCache"/></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>10</value></property>
|
||||||
|
</bean></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>
|
||||||
|
|
||||||
<sect2 id="security-ui-well-known">
|
<sect2 id="security-ui-well-known">
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
</properties>
|
</properties>
|
||||||
<body>
|
<body>
|
||||||
<release version="0.8.0" date="CVS">
|
<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">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">User now accepts blank passwords (null passwords still rejected)</action>
|
||||||
<action dev="benalex" type="update">FilterToBeanProxy now searches hierarchical bean factories</action>
|
<action dev="benalex" type="update">FilterToBeanProxy now searches hierarchical bean factories</action>
|
||||||
|
@ -82,6 +82,11 @@
|
|||||||
protocols or those web applications that prefer a simple browser pop-up
|
protocols or those web applications that prefer a simple browser pop-up
|
||||||
(rather than a form login), Acegi Security can directly process HTTP
|
(rather than a form login), Acegi Security can directly process HTTP
|
||||||
BASIC authentication requests as per RFC 1945.<BR><BR>
|
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
|
<LI><B>Convenient security taglib:</B> Your JSP files can use our taglib
|
||||||
to ensure that protected content like links and messages are only
|
to ensure that protected content like links and messages are only
|
||||||
displayed to users holding the appropriate granted authorities. The taglib
|
displayed to users holding the appropriate granted authorities. The taglib
|
||||||
|
Loading…
x
Reference in New Issue
Block a user