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