View Javadoc

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