/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone.auth; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Random; import java.util.Set; import java.util.StringTokenizer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.w3c.dom.Node; import winstone.AuthenticationPrincipal; import winstone.AuthenticationRealm; import winstone.Logger; import winstone.WinstoneRequest; import winstone.WinstoneResourceBundle; /** * Implements the MD5 digest version of authentication * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: DigestAuthenticationHandler.java,v 1.3 2004/05/22 06:53:45 * rickknowles Exp $ */ public class DigestAuthenticationHandler extends BaseAuthenticationHandler { private MessageDigest md5Digester; public DigestAuthenticationHandler(Node loginConfigNode, List constraintNodes, Set rolesAllowed, AuthenticationRealm realm) throws NoSuchAlgorithmException { super(loginConfigNode, constraintNodes, rolesAllowed, realm); this.md5Digester = MessageDigest.getInstance("MD5"); Logger.log(Logger.DEBUG, AUTH_RESOURCES, "DigestAuthenticationHandler.Initialised", realmName); } /** * Call this once we know that we need to authenticate */ protected void requestAuthentication(HttpServletRequest request, HttpServletResponse response, String pathRequested) throws IOException { // Generate the one time token String oneTimeToken = "WinstoneToken:" + (new Random().nextDouble() * System.currentTimeMillis()); // Need to write the www-authenticate header String authHeader = "Digest realm=\"" + this.realmName + "\", qop=\"auth\", " + "nonce=\"" + oneTimeToken + "\", opaque=\"" + md5Encode(oneTimeToken) + "\""; response.setHeader("WWW-Authenticate", authHeader); // Return unauthorized response.sendError(HttpServletResponse.SC_UNAUTHORIZED, AUTH_RESOURCES .getString("DigestAuthenticationHandler.UnauthorizedMessage")); } /** * Handling the (possible) response * * @return True if the request should continue, or false if we have * intercepted it */ protected boolean validatePossibleAuthenticationResponse( HttpServletRequest request, HttpServletResponse response, String pathRequested) throws IOException { String authorization = request.getHeader("Authorization"); if (authorization == null) return true; // Logger.log(Logger.FULL_DEBUG, "Authorization: " + authorization); if (!authorization.startsWith("Digest")) return true; // Extract tokens from auth string String userName = null; String realm = null; String qop = null; String algorithm = null; String uri = null; String nOnce = null; String nc = null; String cnOnce = null; String clientResponseDigest = null; StringTokenizer st = new StringTokenizer(authorization.substring(6) .trim(), ","); while (st.hasMoreTokens()) { String token = st.nextToken().trim(); int equalPos = token.indexOf('='); String paramName = token.substring(0, equalPos); if (paramName.equals("username")) userName = WinstoneResourceBundle.globalReplace(token .substring(equalPos + 1).trim(), "\"", ""); else if (paramName.equals("realm")) realm = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("qop")) qop = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("algorithm")) algorithm = WinstoneResourceBundle.globalReplace(token .substring(equalPos + 1).trim(), "\"", ""); else if (paramName.equals("uri")) uri = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("nonce")) nOnce = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("nc")) nc = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("cnonce")) cnOnce = WinstoneResourceBundle.globalReplace(token.substring( equalPos + 1).trim(), "\"", ""); else if (paramName.equals("response")) clientResponseDigest = WinstoneResourceBundle.globalReplace( token.substring(equalPos + 1).trim(), "\"", ""); } // Throw out bad attempts if ((userName == null) || (realm == null) || (qop == null) || (uri == null) || (nOnce == null) || (nc == null) || (cnOnce == null) || (clientResponseDigest == null)) return true; else if ((algorithm != null) && !algorithm.equals("MD5")) return true; // Get a user matching the username AuthenticationPrincipal principal = this.realm.retrieveUser(userName); if (principal == null) return true; // Compute the 2 digests and compare String userRealmPasswordDigest = md5Encode(userName + ":" + realm + ":" + principal.getPassword()); String methodURIDigest = md5Encode(request.getMethod() + ":" + uri); String serverResponseDigest = md5Encode(userRealmPasswordDigest + ":" + nOnce + ":" + nc + ":" + cnOnce + ":" + qop + ":" + methodURIDigest); if (serverResponseDigest.equals(clientResponseDigest)) { principal.setAuthType(HttpServletRequest.DIGEST_AUTH); if (request instanceof WinstoneRequest) ((WinstoneRequest) request).setRemoteUser(principal); else if (request instanceof HttpServletRequestWrapper) { HttpServletRequestWrapper wrapper = (HttpServletRequestWrapper) request; if (wrapper.getRequest() instanceof WinstoneRequest) ((WinstoneRequest) wrapper.getRequest()) .setRemoteUser(principal); else Logger.log(Logger.WARNING, AUTH_RESOURCES, "DigestAuthenticationHandler.CantSetUser", wrapper .getRequest().getClass().getName()); } else Logger.log(Logger.WARNING, AUTH_RESOURCES, "DigestAuthenticationHandler.CantSetUser", request .getClass().getName()); } return true; } /** * Returns a hex encoded MD5 digested version of the input string * @param input The string to encode * @return MD5 digested, hex encoded version of the input */ public String md5Encode(String input) throws UnsupportedEncodingException { // Digest byte digestBytes[] = this.md5Digester.digest(input.getBytes("8859_1")); // Write out in hex format char outArray[] = new char[32]; for (int n = 0; n < digestBytes.length; n++) { int hiNibble = (digestBytes[n] & 0xFF) >> 4; int loNibble = (digestBytes[n] & 0xF); outArray[2 * n] = (hiNibble > 9 ? (char) (hiNibble + 87) : (char) (hiNibble + 48)); outArray[2 * n + 1] = (loNibble > 9 ? (char) (loNibble + 87) : (char) (loNibble + 48)); } return new String(outArray); } }