View Javadoc

1   // ========================================================================
2   // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  
14  package org.eclipse.jetty.security.authentication;
15  
16  import java.io.IOException;
17  import java.security.MessageDigest;
18  import java.security.SecureRandom;
19  import java.util.Queue;
20  import java.util.concurrent.ConcurrentHashMap;
21  import java.util.concurrent.ConcurrentLinkedQueue;
22  import java.util.concurrent.ConcurrentMap;
23  import java.util.concurrent.atomic.AtomicInteger;
24  
25  import javax.servlet.ServletRequest;
26  import javax.servlet.ServletResponse;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import org.eclipse.jetty.http.HttpHeaders;
31  import org.eclipse.jetty.http.security.Constraint;
32  import org.eclipse.jetty.http.security.Credential;
33  import org.eclipse.jetty.security.Authenticator.AuthConfiguration;
34  import org.eclipse.jetty.security.SecurityHandler;
35  import org.eclipse.jetty.security.ServerAuthException;
36  import org.eclipse.jetty.security.UserAuthentication;
37  import org.eclipse.jetty.server.Authentication;
38  import org.eclipse.jetty.server.Request;
39  import org.eclipse.jetty.server.UserIdentity;
40  import org.eclipse.jetty.server.Authentication.User;
41  import org.eclipse.jetty.util.B64Code;
42  import org.eclipse.jetty.util.QuotedStringTokenizer;
43  import org.eclipse.jetty.util.StringUtil;
44  import org.eclipse.jetty.util.TypeUtil;
45  import org.eclipse.jetty.util.log.Log;
46  import org.eclipse.jetty.util.log.Logger;
47  
48  /**
49   * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
50   * 
51   * The nonce max age in ms can be set with the {@link SecurityHandler#setInitParameter(String, String)} 
52   * using the name "maxNonceAge"
53   */
54  public class DigestAuthenticator extends LoginAuthenticator
55  {
56      private static final Logger LOG = Log.getLogger(DigestAuthenticator.class);
57      SecureRandom _random = new SecureRandom();
58      private long _maxNonceAgeMs = 60*1000;
59      private ConcurrentMap<String, Nonce> _nonceCount = new ConcurrentHashMap<String, Nonce>();
60      private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<Nonce>();
61      private static class Nonce
62      {
63          final String _nonce;
64          final long _ts;
65          AtomicInteger _nc=new AtomicInteger();
66          public Nonce(String nonce, long ts)
67          {
68              _nonce=nonce;
69              _ts=ts;
70          }
71      }
72  
73      /* ------------------------------------------------------------ */
74      public DigestAuthenticator()
75      {
76          super();
77      }
78  
79      /* ------------------------------------------------------------ */
80      /**
81       * @see org.eclipse.jetty.security.authentication.LoginAuthenticator#setConfiguration(org.eclipse.jetty.security.Authenticator.AuthConfiguration)
82       */
83      @Override
84      public void setConfiguration(AuthConfiguration configuration)
85      {
86          super.setConfiguration(configuration);
87          
88          String mna=configuration.getInitParameter("maxNonceAge");
89          if (mna!=null)
90              _maxNonceAgeMs=Long.valueOf(mna);
91      }
92  
93      /* ------------------------------------------------------------ */
94      public String getAuthMethod()
95      {
96          return Constraint.__DIGEST_AUTH;
97      }
98  
99      /* ------------------------------------------------------------ */
100     public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
101     {
102         return true;
103     }
104 
105     /* ------------------------------------------------------------ */
106     public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
107     {
108         if (!mandatory)
109             return _deferred;
110         
111         HttpServletRequest request = (HttpServletRequest)req;
112         HttpServletResponse response = (HttpServletResponse)res;
113         String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
114 
115         try
116         {
117             boolean stale = false;
118             if (credentials != null)
119             {
120                 if (LOG.isDebugEnabled()) LOG.debug("Credentials: " + credentials);
121                 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
122                 final Digest digest = new Digest(request.getMethod());
123                 String last = null;
124                 String name = null;
125 
126                 while (tokenizer.hasMoreTokens())
127                 {
128                     String tok = tokenizer.nextToken();
129                     char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
130 
131                     switch (c)
132                     {
133                         case '=':
134                             name = last;
135                             last = tok;
136                             break;
137                         case ',':
138                             name = null;
139                             break;
140                         case ' ':
141                             break;
142 
143                         default:
144                             last = tok;
145                             if (name != null)
146                             {
147                                 if ("username".equalsIgnoreCase(name))
148                                     digest.username = tok;
149                                 else if ("realm".equalsIgnoreCase(name))
150                                     digest.realm = tok;
151                                 else if ("nonce".equalsIgnoreCase(name))
152                                     digest.nonce = tok;
153                                 else if ("nc".equalsIgnoreCase(name))
154                                     digest.nc = tok;
155                                 else if ("cnonce".equalsIgnoreCase(name))
156                                     digest.cnonce = tok;
157                                 else if ("qop".equalsIgnoreCase(name))
158                                     digest.qop = tok;
159                                 else if ("uri".equalsIgnoreCase(name))
160                                     digest.uri = tok;
161                                 else if ("response".equalsIgnoreCase(name)) 
162                                     digest.response = tok;
163                                 name=null;
164                             }
165                     }
166                 }
167 
168                 int n = checkNonce(digest,(Request)request);
169 
170                 if (n > 0)
171                 {
172                     UserIdentity user = _loginService.login(digest.username,digest);
173                     if (user!=null)
174                     {
175                         renewSessionOnAuthentication(request,response);
176                         return new UserAuthentication(getAuthMethod(),user);
177                     }
178                 }
179                 else if (n == 0) 
180                     stale = true;
181 
182             }
183 
184             if (!_deferred.isDeferred(response))
185             {
186                 String domain = request.getContextPath();
187                 if (domain == null) 
188                     domain = "/";
189                 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName()
190                         + "\", domain=\""
191                         + domain
192                         + "\", nonce=\""
193                         + newNonce((Request)request)
194                         + "\", algorithm=MD5, qop=\"auth\","
195                         + " stale=" + stale);
196                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
197 
198                 return Authentication.SEND_CONTINUE;
199             }
200 
201             return Authentication.UNAUTHENTICATED;
202         }
203         catch (IOException e)
204         {
205             throw new ServerAuthException(e);
206         }
207 
208     }
209 
210     /* ------------------------------------------------------------ */
211     public String newNonce(Request request)
212     {
213         Nonce nonce;
214         
215         do
216         {
217             byte[] nounce = new byte[24];
218             _random.nextBytes(nounce);
219 
220             nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp());
221         }
222         while (_nonceCount.putIfAbsent(nonce._nonce,nonce)!=null);
223         _nonceQueue.add(nonce);
224                
225         return nonce._nonce;
226     }
227 
228     /**
229      * @param nstring nonce to check
230      * @param request
231      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
232      */
233     /* ------------------------------------------------------------ */
234     private int checkNonce(Digest digest, Request request)
235     {
236         // firstly let's expire old nonces
237         long expired = request.getTimeStamp()-_maxNonceAgeMs;
238         
239         Nonce nonce=_nonceQueue.peek();
240         while (nonce!=null && nonce._ts<expired)
241         {
242             _nonceQueue.remove();
243             _nonceCount.remove(nonce._nonce);
244             nonce=_nonceQueue.peek();
245         }
246         
247        
248         try
249         {
250             nonce = _nonceCount.get(digest.nonce);
251             if (nonce==null)
252                 return 0;
253             
254             long count = Long.parseLong(digest.nc,16);
255             if (count>Integer.MAX_VALUE)
256                 return 0;
257             int old=nonce._nc.get();
258             while (!nonce._nc.compareAndSet(old,(int)count))
259                 old=nonce._nc.get();
260             if (count<=old)
261                 return -1;
262  
263             return 1;
264         }
265         catch (Exception e)
266         {
267             LOG.ignore(e);
268         }
269         return -1;
270     }
271 
272     /* ------------------------------------------------------------ */
273     /* ------------------------------------------------------------ */
274     /* ------------------------------------------------------------ */
275     private static class Digest extends Credential
276     {
277         private static final long serialVersionUID = -2484639019549527724L;
278         final String method;
279         String username = "";
280         String realm = "";
281         String nonce = "";
282         String nc = "";
283         String cnonce = "";
284         String qop = "";
285         String uri = "";
286         String response = "";
287 
288         /* ------------------------------------------------------------ */
289         Digest(String m)
290         {
291             method = m;
292         }
293 
294         /* ------------------------------------------------------------ */
295         @Override
296         public boolean check(Object credentials)
297         {
298             if (credentials instanceof char[])
299                 credentials=new String((char[])credentials);
300             String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
301 
302             try
303             {
304                 MessageDigest md = MessageDigest.getInstance("MD5");
305                 byte[] ha1;
306                 if (credentials instanceof Credential.MD5)
307                 {
308                     // Credentials are already a MD5 digest - assume it's in
309                     // form user:realm:password (we have no way to know since
310                     // it's a digest, alright?)
311                     ha1 = ((Credential.MD5) credentials).getDigest();
312                 }
313                 else
314                 {
315                     // calc A1 digest
316                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
317                     md.update((byte) ':');
318                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
319                     md.update((byte) ':');
320                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
321                     ha1 = md.digest();
322                 }
323                 // calc A2 digest
324                 md.reset();
325                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
326                 md.update((byte) ':');
327                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
328                 byte[] ha2 = md.digest();
329 
330                 // calc digest
331                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
332                 // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
333                 // <">
334                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
335                 // ) > <">
336 
337                 md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1));
338                 md.update((byte) ':');
339                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
340                 md.update((byte) ':');
341                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
342                 md.update((byte) ':');
343                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
344                 md.update((byte) ':');
345                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
346                 md.update((byte) ':');
347                 md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1));
348                 byte[] digest = md.digest();
349 
350                 // check digest
351                 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
352             }
353             catch (Exception e)
354             {
355                 LOG.warn(e);
356             }
357 
358             return false;
359         }
360 
361         public String toString()
362         {
363             return username + "," + response;
364         }
365     }
366 }