// ======================================================================== // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // You may elect to redistribute this code under either of these licenses. // ======================================================================== package org.eclipse.jetty.security.authentication; import java.io.IOException; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpHeaders; import org.eclipse.jetty.security.SecurityHandler; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication.User; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Credential; /** * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $ * * The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)} * using the name "maxNonceAge" */ public class DigestAuthenticator extends LoginAuthenticator { private static final Logger LOG = Log.getLogger(DigestAuthenticator.class); SecureRandom _random = new SecureRandom(); private long _maxNonceAgeMs = 60*1000; private ConcurrentMap<String, Nonce> _nonceCount = new ConcurrentHashMap<String, Nonce>(); private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<Nonce>(); private static class Nonce { final String _nonce; final long _ts; AtomicInteger _nc=new AtomicInteger(); public Nonce(String nonce, long ts) { _nonce=nonce; _ts=ts; } } /* ------------------------------------------------------------ */ public DigestAuthenticator() { super(); } /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration) */ @Override public void setConfiguration(AuthConfiguration configuration) { super.setConfiguration(configuration); String mna=configuration.getInitParameter("maxNonceAge"); if (mna!=null) { synchronized (this) { _maxNonceAgeMs=Long.valueOf(mna); } } } /* ------------------------------------------------------------ */ public synchronized void setMaxNonceAge(long maxNonceAgeInMillis) { _maxNonceAgeMs = maxNonceAgeInMillis; } /* ------------------------------------------------------------ */ public String getAuthMethod() { return Constraint.__DIGEST_AUTH; } /* ------------------------------------------------------------ */ public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException { return true; } /* ------------------------------------------------------------ */ public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException { if (!mandatory) return _deferred; HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; String credentials = request.getHeader(HttpHeaders.AUTHORIZATION); try { boolean stale = false; if (credentials != null) { if (LOG.isDebugEnabled()) LOG.debug("Credentials: " + credentials); QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false); final Digest digest = new Digest(request.getMethod()); String last = null; String name = null; while (tokenizer.hasMoreTokens()) { String tok = tokenizer.nextToken(); char c = (tok.length() == 1) ? tok.charAt(0) : '\0'; switch (c) { case '=': name = last; last = tok; break; case ',': name = null; break; case ' ': break; default: last = tok; if (name != null) { if ("username".equalsIgnoreCase(name)) digest.username = tok; else if ("realm".equalsIgnoreCase(name)) digest.realm = tok; else if ("nonce".equalsIgnoreCase(name)) digest.nonce = tok; else if ("nc".equalsIgnoreCase(name)) digest.nc = tok; else if ("cnonce".equalsIgnoreCase(name)) digest.cnonce = tok; else if ("qop".equalsIgnoreCase(name)) digest.qop = tok; else if ("uri".equalsIgnoreCase(name)) digest.uri = tok; else if ("response".equalsIgnoreCase(name)) digest.response = tok; name=null; } } } int n = checkNonce(digest,(Request)request); if (n > 0) { UserIdentity user = _loginService.login(digest.username,digest); if (user!=null) { renewSessionOnAuthentication(request,response); return new UserAuthentication(getAuthMethod(),user); } } else if (n == 0) stale = true; } if (!_deferred.isDeferred(response)) { String domain = request.getContextPath(); if (domain == null) domain = "/"; response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName() + "\", domain=\"" + domain + "\", nonce=\"" + newNonce((Request)request) + "\", algorithm=MD5, qop=\"auth\"," + " stale=" + stale); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return Authentication.SEND_CONTINUE; } return Authentication.UNAUTHENTICATED; } catch (IOException e) { throw new ServerAuthException(e); } } /* ------------------------------------------------------------ */ public String newNonce(Request request) { Nonce nonce; do { byte[] nounce = new byte[24]; _random.nextBytes(nounce); nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp()); } while (_nonceCount.putIfAbsent(nonce._nonce,nonce)!=null); _nonceQueue.add(nonce); return nonce._nonce; } /** * @param nstring nonce to check * @param request * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce */ /* ------------------------------------------------------------ */ private int checkNonce(Digest digest, Request request) { // firstly let's expire old nonces long expired; synchronized (this) { expired = request.getTimeStamp()-_maxNonceAgeMs; } Nonce nonce=_nonceQueue.peek(); while (nonce!=null && nonce._ts<expired) { _nonceQueue.remove(); _nonceCount.remove(nonce._nonce); nonce=_nonceQueue.peek(); } try { nonce = _nonceCount.get(digest.nonce); if (nonce==null) return 0; long count = Long.parseLong(digest.nc,16); if (count>Integer.MAX_VALUE) return 0; int old=nonce._nc.get(); while (!nonce._nc.compareAndSet(old,(int)count)) old=nonce._nc.get(); if (count<=old) return -1; return 1; } catch (Exception e) { LOG.ignore(e); } return -1; } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ private static class Digest extends Credential { private static final long serialVersionUID = -2484639019549527724L; final String method; String username = ""; String realm = ""; String nonce = ""; String nc = ""; String cnonce = ""; String qop = ""; String uri = ""; String response = ""; /* ------------------------------------------------------------ */ Digest(String m) { method = m; } /* ------------------------------------------------------------ */ @Override public boolean check(Object credentials) { if (credentials instanceof char[]) credentials=new String((char[])credentials); String password = (credentials instanceof String) ? (String) credentials : credentials.toString(); try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] ha1; if (credentials instanceof Credential.MD5) { // Credentials are already a MD5 digest - assume it's in // form user:realm:password (we have no way to know since // it's a digest, alright?) ha1 = ((Credential.MD5) credentials).getDigest(); } else { // calc A1 digest md.update(username.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(realm.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(password.getBytes(StringUtil.__ISO_8859_1)); ha1 = md.digest(); } // calc A2 digest md.reset(); md.update(method.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(uri.getBytes(StringUtil.__ISO_8859_1)); byte[] ha2 = md.digest(); // calc digest // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) // <"> // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) // ) > <"> md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(nonce.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(nc.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(cnonce.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(qop.getBytes(StringUtil.__ISO_8859_1)); md.update((byte) ':'); md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1)); byte[] digest = md.digest(); // check digest return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response)); } catch (Exception e) { LOG.warn(e); } return false; } public String toString() { return username + "," + response; } } }