/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.security; import java.io.IOException; import java.security.Principal; import java.text.CharacterIterator; import java.util.logging.Level; import javax.enterprise.context.ApplicationScoped; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.caucho.util.Base64; import com.caucho.util.CharBuffer; import com.caucho.util.CharCursor; import com.caucho.util.RandomUtil; import com.caucho.util.StringCharCursor; import com.caucho.xml.XmlChar; /** * Implements the "digest" auth-method. Basic uses the * HTTP authentication with WWW-Authenticate and SC_UNAUTHORIZE. * * The HTTP Digest authentication uses the following algorithm * to calculate the digest. The digest is then compared to * the client digest. * * <code><pre> * A1 = MD5(username + ':' + realm + ':' + password) * A2 = MD5(method + ':' + uri) * digest = MD5(A1 + ':' + nonce + A2) * </pre></code> */ @ApplicationScoped public class DigestLogin extends AbstractLogin { protected String _realm; public DigestLogin() { } /** * Sets the login realm. */ public void setRealmName(String realm) { _realm = realm; } /** * Gets the realm. */ public String getRealmName() { return _realm; } /** * Returns the authentication type. */ public String getAuthType() { return "Digest"; } /** * Returns the principal from a digest authentication * * @param auth the authenticator for this application. */ @Override protected Principal getUserPrincipalImpl(HttpServletRequest request) { String value = request.getHeader("authorization"); if (value == null) return null; String username = null; String realm = null; String uri = null; String nonce = null; String cnonce = null; String nc = null; String qop = null; String digest = null; CharCursor cursor = new StringCharCursor(value); String key = scanKey(cursor); if (! "Digest".equalsIgnoreCase(key)) return null; while ((key = scanKey(cursor)) != null) { value = scanValue(cursor); if (key.equals("username")) username = value; else if (key.equals("realm")) realm = value; else if (key.equals("uri")) uri = value; else if (key.equals("nonce")) nonce = value; else if (key.equals("response")) digest = value; else if (key.equals("cnonce")) cnonce = value; else if (key.equals("nc")) nc = value; else if (key.equals("qop")) qop = value; } byte []clientDigest = decodeDigest(digest); if (clientDigest == null || username == null || uri == null || nonce == null) return null; Authenticator auth = getAuthenticator(); Principal principal = new BasicPrincipal(username); HttpDigestCredentials cred = new HttpDigestCredentials(); cred.setCnonce(cnonce); cred.setMethod(request.getMethod()); cred.setNc(nc); cred.setNonce(nonce); cred.setQop(qop); cred.setRealm(realm); cred.setResponse(clientDigest); cred.setUri(uri); Principal user; user = auth.authenticate(principal, cred, request); if (log.isLoggable(Level.FINE)) log.fine("digest: " + username + " -> " + user); return user; } /** * Sends a challenge for basic authentication. */ protected void loginChallenge(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String realm = getRealmName(); if (realm == null) realm = "resin"; StringBuilder cb = new StringBuilder(); Base64.encode(cb, RandomUtil.getRandomLong()); String nonce = cb.toString(); cb.setLength(0); cb.append("Digest "); cb.append("realm=\""); cb.append(realm); cb.append("\", qop=\"auth\", "); cb.append("nonce=\""); cb.append(nonce); cb.append("\""); res.setHeader("WWW-Authenticate", cb.toString()); res.sendError(HttpServletResponse.SC_UNAUTHORIZED); } protected long getRandomLong(ServletContext application) { return RandomUtil.getRandomLong(); } protected byte []decodeDigest(String digest) { if (digest == null) return null; int len = (digest.length() + 1) / 2; byte []clientDigest = new byte[len]; for (int i = 0; i + 1 < digest.length(); i += 2) { int ch1 = digest.charAt(i); int ch2 = digest.charAt(i + 1); int b = 0; if (ch1 >= '0' && ch1 <= '9') b += ch1 - '0'; else if (ch1 >= 'a' && ch1 <= 'f') b += ch1 - 'a' + 10; b *= 16; if (ch2 >= '0' && ch2 <= '9') b += ch2 - '0'; else if (ch2 >= 'a' && ch2 <= 'f') b += ch2 - 'a' + 10; clientDigest[i / 2] = (byte) b; } return clientDigest; } protected String scanKey(CharCursor cursor) { int ch; while (XmlChar.isWhitespace((ch = cursor.current())) || ch == ',') { cursor.next(); } ch = cursor.current(); if (ch == CharacterIterator.DONE) return null; if (! XmlChar.isNameStart(ch)) throw new RuntimeException("bad key: " + (char) ch + " " + cursor); CharBuffer cb = CharBuffer.allocate(); while (XmlChar.isNameChar(ch = cursor.read())) { cb.append((char) ch); } if (ch != CharacterIterator.DONE) cursor.previous(); return cb.close(); } protected String scanValue(CharCursor cursor) { int ch; skipWhitespace(cursor); ch = cursor.read(); if (ch != '=') throw new RuntimeException("expected '='"); skipWhitespace(cursor); CharBuffer cb = CharBuffer.allocate(); ch = cursor.read(); if (ch == '"') while ((ch = cursor.read()) != CharacterIterator.DONE && ch != '"') cb.append((char) ch); else { for (; ch != CharacterIterator.DONE && ch != ',' && ! XmlChar.isWhitespace(ch); ch = cursor.read()) cb.append((char) ch); if (ch != CharacterIterator.DONE) cursor.previous(); } return cb.close(); } protected void skipWhitespace(CharCursor cursor) { while (XmlChar.isWhitespace(cursor.current())) { cursor.next(); } } }