/* * eXist Open Source Native XML Database * Copyright (C) 2001-2008 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $Id$ */ package org.exist.http.servlets; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.exist.security.MessageDigester; import org.exist.security.SecurityManager; import org.exist.security.Subject; import org.exist.security.internal.AccountImpl; import org.exist.security.internal.SubjectAccreditedImpl; import org.exist.storage.BrokerPool; import static java.nio.charset.StandardCharsets.ISO_8859_1; /** * An Authenticator that uses MD5 Digest Authentication. * * @author wolf */ public class DigestAuthenticator implements Authenticator { private BrokerPool pool; public DigestAuthenticator(BrokerPool pool) { this.pool = pool; } public Subject authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException { return authenticate(request, response, true); } @Override public Subject authenticate( HttpServletRequest request, HttpServletResponse response, boolean sendChallenge) throws IOException { final String credentials = request.getHeader("Authorization"); if (credentials == null) { sendChallenge(request, response); return null; } final Digest digest = new Digest(request.getMethod()); parseCredentials(digest, credentials); final SecurityManager secman = pool.getSecurityManager(); final AccountImpl user = (AccountImpl)secman.getAccount(digest.username); if (user == null) { // If user does not exist then send a challenge request again if (sendChallenge) {sendChallenge(request, response);} return null; } if (!digest.check(user.getDigestPassword())) { // If password is incorrect then send a challenge request again if (sendChallenge) {sendChallenge(request, response);} return null; } return new SubjectAccreditedImpl(user, this); } @Override public void sendChallenge(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setHeader("WWW-Authenticate", "Digest realm=\"exist\", " + "nonce=\"" + createNonce(request) + "\", " + "domain=\"" + request.getContextPath() + "\", " + "opaque=\"" + MessageDigester.md5(Integer.toString(hashCode(), 27), false) + '"'); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } private String createNonce(HttpServletRequest request) { return MessageDigester.md5(request.getRemoteAddr() + ':' + Long.toString(System.currentTimeMillis()) + ':' + Integer.toString(hashCode()), false); } private static void parseCredentials(Digest digest, String credentials) { credentials = credentials.substring("Digest ".length()); final StringBuilder current = new StringBuilder(); String name = null, value; boolean inQuotedString = false; for (int i = 0; i < credentials.length(); i++) { final char ch = credentials.charAt(i); switch (ch) { case ' ': break; case '"': case '\'': if (inQuotedString) { value = current.toString(); current.setLength(0); inQuotedString = false; if ("username".equalsIgnoreCase(name)) {digest.username = value;} else if ("realm".equalsIgnoreCase(name)) {digest.realm = value;} else if ("nonce".equalsIgnoreCase(name)) {digest.nonce = value;} else if ("uri".equalsIgnoreCase(name)) {digest.uri = value;} else if ("response".equalsIgnoreCase(name)) {digest.response = value;} } else { value = null; inQuotedString = true; } break; case ',': name = null; break; case '=': name = current.toString(); current.setLength(0); break; default: current.append(ch); break; } } } private static class Digest { String method = null; String username = null; @SuppressWarnings("unused") String realm = null; String nonce = null; String uri = null; String response = null; public Digest(String method) { this.method = method; } public boolean check(String credentials) throws IOException { if (credentials == null) // no password set for the user: return true {return true;} try { final MessageDigest md = MessageDigest.getInstance("MD5"); // calc A2 digest md.reset(); md.update(method.getBytes(ISO_8859_1)); md.update((byte) ':'); md.update(uri.getBytes(ISO_8859_1)); final byte[] ha2 = md.digest(); // calc digest md.update(credentials.getBytes(ISO_8859_1)); md.update((byte) ':'); md.update(nonce.getBytes(ISO_8859_1)); md.update((byte) ':'); md.update(MessageDigester.byteArrayToHex(ha2).getBytes(ISO_8859_1)); final byte[] digest = md.digest(); // check digest return (MessageDigester.byteArrayToHex(digest).equalsIgnoreCase(response)); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException("MD5 not supported"); } } } }