Java tutorial
/* Copyright 2004, 2005, 2006 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 org.sipfoundry.sipxconfig.security; import org.acegisecurity.AcegiMessageSource; import org.acegisecurity.AuthenticationException; import org.acegisecurity.AuthenticationServiceException; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContextHolder; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.providers.dao.UserCache; import org.acegisecurity.providers.dao.cache.NullUserCache; import org.acegisecurity.ui.AuthenticationDetailsSource; import org.acegisecurity.ui.AuthenticationDetailsSourceImpl; import org.acegisecurity.ui.digestauth.DigestProcessingFilterEntryPoint; import org.acegisecurity.ui.digestauth.NonceExpiredException; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UserDetailsService; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.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.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.util.Assert; 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>SecurityContextHolder</code>.<p>For a detailed background on what this filter is designed to process, * refer to <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a> (which superseded RFC 2069, although this * 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 * DigestProcessingFilterEntryPoint}.</p> * <P>If authentication is successful, the resulting {@link org.acegisecurity.Authentication Authentication} * object will be placed into the <code>SecurityContextHolder</code>.</p> * <p>If authentication fails, an {@link org.acegisecurity.ui.AuthenticationEntryPoint AuthenticationEntryPoint} * implementation is called. This must always be {@link DigestProcessingFilterEntryPoint}, which will prompt the user * 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 * org.acegisecurity.util.FilterToBeanProxy}.</p> * * Code copied from org.acegisecurity.ui.digestauth.DigestProcessingFilter * Sipxconfig custom code inside START SIPXECS CUSTOM CODE / END SIPXECS CUSTOM CODE: * create DigestUsernamePasswordAuthenticationToken, Used in conjunction with * org.sipfoundry.sipxconfig.security.DaoAuthenticationProvider that handles * this type of tokens */ public class DigestProcessingFilter implements Filter, InitializingBean, MessageSourceAware { //~ Static fields/initializers ===================================================================================== private static final Log logger = LogFactory.getLog(DigestProcessingFilter.class); //~ Instance fields ================================================================================================ private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl(); private DigestProcessingFilterEntryPoint authenticationEntryPoint; protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor(); private UserCache userCache = new NullUserCache(); private UserDetailsService userDetailsService; private boolean passwordAlreadyEncoded = false; //~ Methods ======================================================================================================== public void afterPropertiesSet() throws Exception { Assert.notNull(userDetailsService, "A UserDetailsService is required"); Assert.notNull(authenticationEntryPoint, "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 = StringSplitUtils.splitIgnoringQuotes(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(messages.getMessage("DigestProcessingFilter.missingMandatory", new Object[] { section212response }, "Missing mandatory digest value; received header {0}"))); 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(messages.getMessage("DigestProcessingFilter.missingAuth", new Object[] { section212response }, "Missing mandatory digest value; received header {0}"))); return; } } // Check realm name equals what we expected if (!this.getAuthenticationEntryPoint().getRealmName().equals(realm)) { fail(request, response, new BadCredentialsException(messages.getMessage("DigestProcessingFilter.incorrectRealm", new Object[] { realm, this.getAuthenticationEntryPoint().getRealmName() }, "Response realm name '{0}' does not match system realm name of '{1}'"))); return; } // Check nonce was a Base64 encoded (as sent by DigestProcessingFilterEntryPoint) if (!Base64.isArrayByteBase64(nonce.getBytes())) { fail(request, response, new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceEncoding", new Object[] { nonce }, "Nonce is not encoded in Base64; received nonce {0}"))); 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(messages.getMessage("DigestProcessingFilter.nonceNotTwoTokens", new Object[] { nonceAsPlainText }, "Nonce should have yielded two tokens but was {0}"))); return; } // Extract expiry time from nonce long nonceExpiryTime; try { nonceExpiryTime = new Long(nonceTokens[0]).longValue(); } catch (NumberFormatException nfe) { fail(request, response, new BadCredentialsException(messages.getMessage("DigestProcessingFilter.nonceNotNumeric", new Object[] { nonceAsPlainText }, "Nonce token should have yielded a numeric first token, but was {0}"))); 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(messages.getMessage("DigestProcessingFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}"))); return; } // Lookup password for presented username // NB: DAO-provided password MUST be clear text - not encoded/salted // (unless this instance's passwordAlreadyEncoded property is 'false') boolean loadedFromDao = false; UserDetails user = userCache.getUserFromCache(username); if (user == null) { loadedFromDao = true; try { user = userDetailsService.loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { fail(request, response, new BadCredentialsException( messages.getMessage("DigestProcessingFilter.usernameNotFound", new Object[] { username }, "Username {0} not found"))); 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(passwordAlreadyEncoded, 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 = userDetailsService.loadUserByUsername(username); } catch (UsernameNotFoundException notFound) { // Would very rarely happen, as user existed earlier fail(request, response, new BadCredentialsException( messages.getMessage("DigestProcessingFilter.usernameNotFound", new Object[] { username }, "Username {0} not found"))); } userCache.putUserInCache(user); // Don't catch IllegalArgumentException (already checked validity) serverDigestMd5 = generateDigest(passwordAlreadyEncoded, 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( messages.getMessage("DigestProcessingFilter.incorrectResponse", "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( messages.getMessage("DigestProcessingFilter.nonceExpired", "Nonce has expired/timed out"))); return; } if (logger.isDebugEnabled()) { logger.debug("Authentication success for user: '" + username + "' with response: '" + responseDigest + "'"); } // START SIPXECS CUSTOM CODE: XX-8253 // commented original code //UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user, // user.getPassword()); // creates digest token to be handled by org.sipfoundry.sipxconfig.security.DaoAuthenticationProvider UsernamePasswordAuthenticationToken authRequest = new DigestUsernamePasswordAuthenticationToken(user, user.getPassword()); // END SIPXECS CUSTOM CODE: XX-8253 authRequest.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request)); SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request, response); } public static String encodePasswordInA1Format(String username, String realm, String password) { String a1 = username + ":" + realm + ":" + password; String a1Md5 = new String(DigestUtils.md5Hex(a1)); return a1Md5; } private void fail(ServletRequest request, ServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(null); if (logger.isDebugEnabled()) { logger.debug(failed); } authenticationEntryPoint.commence(request, response, failed); } /** * 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 simplify the * coding of user agents. * * @param passwordAlreadyEncoded true if the password argument is already encoded in the correct format. False if * it is plain text. * @param username the user's login name. * @param realm the name of the realm. * @param password the user's password in plaintext or ready-encoded. * @param httpMethod the HTTP request method (GET, POST etc.) * @param uri the request URI. * @param qop the qop directive, or null if not set. * @param nonce the nonce supplied by the server * @param nc the "nonce-count" as defined in RFC 2617. * @param cnonce opaque string supplied by the client when qop is set. * @return the MD5 of the digest authentication response, encoded in hex * @throws IllegalArgumentException if the supplied qop value is unsupported. */ public static String generateDigest(boolean passwordAlreadyEncoded, String username, String realm, String password, String httpMethod, String uri, String qop, String nonce, String nc, String cnonce) throws IllegalArgumentException { String a1Md5 = null; String a2 = httpMethod + ":" + uri; String a2Md5 = new String(DigestUtils.md5Hex(a2)); if (passwordAlreadyEncoded) { a1Md5 = password; } else { a1Md5 = encodePasswordInA1Format(username, realm, password); } 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 DigestProcessingFilterEntryPoint getAuthenticationEntryPoint() { return authenticationEntryPoint; } public UserCache getUserCache() { return userCache; } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void init(FilterConfig ignored) throws ServletException { } public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setAuthenticationEntryPoint(DigestProcessingFilterEntryPoint authenticationEntryPoint) { this.authenticationEntryPoint = authenticationEntryPoint; } public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); } public void setPasswordAlreadyEncoded(boolean passwordAlreadyEncoded) { this.passwordAlreadyEncoded = passwordAlreadyEncoded; } public void setUserCache(UserCache userCache) { this.userCache = userCache; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }