Java tutorial
/** * Project: rms-server * File: EventPublisherAwareDigestAuthenticationFilter.java * License: * This file is licensed under GNU General Public License version 3 * http://www.gnu.org/licenses/gpl-3.0.txt * * Copyright: Bartosz Cichecki [ cichecki.bartosz@gmail.com ] * Date: 28-08-2012 */ package pl.bcichecki.rms.customizations.org.springframework.security.web.authentication.www; import java.io.IOException; import java.util.ArrayList; import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.cache.NullUserCache; import org.springframework.security.crypto.codec.Base64; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.NonceExpiredException; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.filter.GenericFilterBean; /** * <b>IMPORTANT NOTICE!</b> * <p> * This class is exact copy of {@link org.springframework.security.web.authentication.www.DigestAuthenticationFilter} beside parts * responsible for publishing events and is redefined here only because it was impossible to override the original one effectively. needs * it. You <b>ought to</b> use the original one if you don't the <code>AuthenticationEventPublisher</code>! * <p> * <i>Copied from Spring Security 3.1.2.RELEASE</i> * <p> * 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> * 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> * 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 DigestAuthenticationEntryPoint}. * <p> * If authentication is successful, the resulting {@link org.springframework.security.core.Authentication Authentication} object will be * placed into the <code>SecurityContextHolder</code>. * <p> * If authentication fails, an {@link org.springframework.security.web.AuthenticationEntryPoint AuthenticationEntryPoint} implementation is * called. This must always be {@link DigestAuthenticationEntryPoint}, which will prompt the user to authenticate again via Digest * authentication. * <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. * * @author Ben Alex * @author Luke Taylor * @author Bartosz Cichecki * @since 1.0.0 */ public class EventPublisherAwareDigestAuthenticationFilter extends GenericFilterBean implements MessageSourceAware { private class DigestData { private final String username; private final String realm; private final String nonce; private final String uri; private final String response; private final String qop; private final String nc; private final String cnonce; private final String section212response; private long nonceExpiryTime; DigestData(String header) { section212response = header.substring(7); String[] headerEntries = DigestAuthUtils.splitIgnoringQuotes(section212response, ','); Map<String, String> headerMap = DigestAuthUtils.splitEachArrayElementAndCreateMap(headerEntries, "=", "\""); username = headerMap.get("username"); realm = headerMap.get("realm"); nonce = headerMap.get("nonce"); uri = headerMap.get("uri"); response = headerMap.get("response"); qop = headerMap.get("qop"); // RFC 2617 extension nc = headerMap.get("nc"); // RFC 2617 extension cnonce = headerMap.get("cnonce"); // RFC 2617 extension if (logger.isDebugEnabled()) { logger.debug("Extracted username: '" + username + "'; realm: '" + realm + "'; nonce: '" + nonce + "'; uri: '" + uri + "'; response: '" + response + "'"); } } String calculateServerDigest(String password, String httpMethod) { // Compute the expected response-digest (will be in hex form) // Don't catch IllegalArgumentException (already checked validity) return DigestAuthUtils.generateDigest(passwordAlreadyEncoded, username, realm, password, httpMethod, uri, qop, nonce, nc, cnonce); } String getResponse() { return response; } String getUsername() { return username; } boolean isNonceExpired() { long now = System.currentTimeMillis(); return nonceExpiryTime < now; } void validateAndDecode(String entryPointKey, String expectedRealm) throws BadCredentialsException { // Check all required parameters were supplied (ie RFC 2069) if (username == null || realm == null || nonce == null || uri == null || response == null) { throw new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.missingMandatory", new Object[] { section212response }, "Missing mandatory digest value; received header {0}")); } // 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 + "'"); } throw new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.missingAuth", new Object[] { section212response }, "Missing mandatory digest value; received header {0}")); } } // Check realm name equals what we expected if (!expectedRealm.equals(realm)) { throw new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.incorrectRealm", new Object[] { realm, expectedRealm }, "Response realm name '{0}' does not match system realm name of '{1}'")); } // Check nonce was Base64 encoded (as sent by // DigestAuthenticationEntryPoint) if (!Base64.isBase64(nonce.getBytes())) { throw new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.nonceEncoding", new Object[] { nonce }, "Nonce is not encoded in Base64; received nonce {0}")); } // Decode nonce from Base64 // format of nonce is: // base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) String nonceAsPlainText = new String(Base64.decode(nonce.getBytes())); String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":"); if (nonceTokens.length != 2) { throw new BadCredentialsException(messages.getMessage( "DigestAuthenticationFilter.nonceNotTwoTokens", new Object[] { nonceAsPlainText }, "Nonce should have yielded two tokens but was {0}")); } // Extract expiry time from nonce try { nonceExpiryTime = new Long(nonceTokens[0]).longValue(); } catch (NumberFormatException nfe) { throw new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.nonceNotNumeric", new Object[] { nonceAsPlainText }, "Nonce token should have yielded a numeric first token, but was {0}")); } // Check signature of nonce matches this expiry time String expectedNonceSignature = DigestAuthUtils.md5Hex(nonceExpiryTime + ":" + entryPointKey); if (!expectedNonceSignature.equals(nonceTokens[1])) { new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText }, "Nonce token compromised {0}")); } } } // ~ Static fields/initializers // ===================================================================================== private static final Log logger = LogFactory.getLog(EventPublisherAwareDigestAuthenticationFilter.class); // ~ Instance fields // ================================================================================================ private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); private DigestAuthenticationEntryPoint authenticationEntryPoint; protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private UserCache userCache = new NullUserCache(); private UserDetailsService userDetailsService; private boolean passwordAlreadyEncoded = false; // MODIFICATION private AuthenticationEventPublisher authenticationEventPublisher; // END OF MODIFICATION // ~ Methods // ======================================================================================================== private boolean createAuthenticatedToken = false; @Override public void afterPropertiesSet() { Assert.notNull(userDetailsService, "A UserDetailsService is required"); Assert.notNull(authenticationEntryPoint, "A DigestAuthenticationEntryPoint is required"); // MODIFICATION Assert.notNull(authenticationEventPublisher, "A AuthenticationEventPublisher is required"); // END OF MODIFICATION } private Authentication createSuccessfulAuthentication(HttpServletRequest request, UserDetails user) { UsernamePasswordAuthenticationToken authRequest; if (createAuthenticatedToken) { authRequest = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()); } else { authRequest = new UsernamePasswordAuthenticationToken(user, user.getPassword()); } authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); return authRequest; } // MODIFICATION private Authentication createUnsuccessfulAuthentication(HttpServletRequest request, UserDetails user) { Authentication authentication = createSuccessfulAuthentication(request, user); authentication.setAuthenticated(false); return authentication; } // END OF MODIFICATION @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Digest ")) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Digest Authorization header received from user agent: " + header); } DigestData digestAuth = new DigestData(header); try { digestAuth.validateAndDecode(authenticationEntryPoint.getKey(), authenticationEntryPoint.getRealmName()); } catch (BadCredentialsException e) { fail(request, response, e); 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 cacheWasUsed = true; UserDetails user = userCache.getUserFromCache(digestAuth.getUsername()); String serverDigestMd5; try { if (user == null) { cacheWasUsed = false; user = userDetailsService.loadUserByUsername(digestAuth.getUsername()); if (user == null) { throw new AuthenticationServiceException( "AuthenticationDao returned null, which is an interface contract violation"); } userCache.putUserInCache(user); } serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(), request.getMethod()); // If digest is incorrect, try refreshing from backend and // recomputing if (!serverDigestMd5.equals(digestAuth.getResponse()) && cacheWasUsed) { if (logger.isDebugEnabled()) { logger.debug( "Digest comparison failure; trying to refresh user from DAO in case password had changed"); } user = userDetailsService.loadUserByUsername(digestAuth.getUsername()); userCache.putUserInCache(user); serverDigestMd5 = digestAuth.calculateServerDigest(user.getPassword(), request.getMethod()); } } catch (UsernameNotFoundException notFound) { // MODIFICATION boolean userWasNull = false; if (user == null) { userWasNull = true; user = new User(digestAuth.getUsername(), "fakePassSoSpringShutUp", false, false, false, false, new ArrayList<GrantedAuthority>()); } authenticationEventPublisher.publishAuthenticationFailure(notFound, createUnsuccessfulAuthentication(request, user)); if (userWasNull) { user = null; } // END OF MODIFICATION fail(request, response, new BadCredentialsException(messages.getMessage("DigestAuthenticationFilter.usernameNotFound", new Object[] { digestAuth.getUsername() }, "Username {0} not found"))); return; } // If digest is still incorrect, definitely reject authentication // attempt if (!serverDigestMd5.equals(digestAuth.getResponse())) { if (logger.isDebugEnabled()) { logger.debug("Expected response: '" + serverDigestMd5 + "' but received: '" + digestAuth.getResponse() + "'; is AuthenticationDao returning clear text passwords?"); } // MODIFICATION authenticationEventPublisher.publishAuthenticationFailure( new BadCredentialsException("Bad credentials"), createUnsuccessfulAuthentication(request, user)); // END OF MODIFICATION fail(request, response, new BadCredentialsException( messages.getMessage("DigestAuthenticationFilter.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 (digestAuth.isNonceExpired()) { fail(request, response, new NonceExpiredException( messages.getMessage("DigestAuthenticationFilter.nonceExpired", "Nonce has expired/timed out"))); return; } if (logger.isDebugEnabled()) { logger.debug("Authentication success for user: '" + digestAuth.getUsername() + "' with response: '" + digestAuth.getResponse() + "'"); } SecurityContextHolder.getContext().setAuthentication(createSuccessfulAuthentication(request, user)); chain.doFilter(request, response); } private void fail(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(null); if (logger.isDebugEnabled()) { logger.debug(failed); } authenticationEntryPoint.commence(request, response, failed); } protected final DigestAuthenticationEntryPoint getAuthenticationEntryPoint() { return authenticationEntryPoint; } public UserCache getUserCache() { return userCache; } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } public void setAuthenticationEntryPoint(DigestAuthenticationEntryPoint authenticationEntryPoint) { this.authenticationEntryPoint = authenticationEntryPoint; } // MODIFICATION public void setAuthenticationEventPublisher(AuthenticationEventPublisher authenticationEventPublisher) { this.authenticationEventPublisher = authenticationEventPublisher; } // END OF MODIFICATION /** * If you set this property, the Authentication object, which is created after the successful digest authentication will be marked as * <b>authenticated</b> and filled with the authorities loaded by the UserDetailsService. It therefore will not be re-authenticated by * your AuthenticationProvider. This means, that only the password of the user is checked, but not the flags like isEnabled() or * isAccountNonExpired(). You will save some time by enabling this flag, as otherwise your UserDetailsService will be called twice. A * more secure option would be to introduce a cache around your UserDetailsService, but if you don't use these flags, you can also * safely enable this option. * * @param createAuthenticatedToken * default is false */ public void setCreateAuthenticatedToken(boolean createAuthenticatedToken) { this.createAuthenticatedToken = createAuthenticatedToken; } @Override public void setMessageSource(MessageSource messageSource) { 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; } }