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()) 
121                     LOG.debug("Credentials: " + credentials);
122                 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
123                 final Digest digest = new Digest(request.getMethod());
124                 String last = null;
125                 String name = null;
126 
127                 while (tokenizer.hasMoreTokens())
128                 {
129                     String tok = tokenizer.nextToken();
130                     char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
131 
132                     switch (c)
133                     {
134                         case '=':
135                             name = last;
136                             last = tok;
137                             break;
138                         case ',':
139                             name = null;
140                             break;
141                         case ' ':
142                             break;
143 
144                         default:
145                             last = tok;
146                             if (name != null)
147                             {
148                                 if ("username".equalsIgnoreCase(name))
149                                     digest.username = tok;
150                                 else if ("realm".equalsIgnoreCase(name))
151                                     digest.realm = tok;
152                                 else if ("nonce".equalsIgnoreCase(name))
153                                     digest.nonce = tok;
154                                 else if ("nc".equalsIgnoreCase(name))
155                                     digest.nc = tok;
156                                 else if ("cnonce".equalsIgnoreCase(name))
157                                     digest.cnonce = tok;
158                                 else if ("qop".equalsIgnoreCase(name))
159                                     digest.qop = tok;
160                                 else if ("uri".equalsIgnoreCase(name))
161                                     digest.uri = tok;
162                                 else if ("response".equalsIgnoreCase(name)) 
163                                     digest.response = tok;
164                                 name=null;
165                             }
166                     }
167                 }
168 
169                 int n = checkNonce(digest,(Request)request);
170 
171                 if (n > 0)
172                 {
173                     UserIdentity user = _loginService.login(digest.username,digest);
174                     if (user!=null)
175                     {
176                         renewSessionOnAuthentication(request,response);
177                         return new UserAuthentication(getAuthMethod(),user);
178                     }
179                 }
180                 else if (n == 0) 
181                     stale = true;
182 
183             }
184 
185             if (!_deferred.isDeferred(response))
186             {
187                 String domain = request.getContextPath();
188                 if (domain == null) 
189                     domain = "/";
190                 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName()
191                         + "\", domain=\""
192                         + domain
193                         + "\", nonce=\""
194                         + newNonce((Request)request)
195                         + "\", algorithm=MD5, qop=\"auth\","
196                         + " stale=" + stale);
197                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
198 
199                 return Authentication.SEND_CONTINUE;
200             }
201 
202             return Authentication.UNAUTHENTICATED;
203         }
204         catch (IOException e)
205         {
206             throw new ServerAuthException(e);
207         }
208 
209     }
210 
211     /* ------------------------------------------------------------ */
212     public String newNonce(Request request)
213     {
214         Nonce nonce;
215         
216         do
217         {
218             byte[] nounce = new byte[24];
219             _random.nextBytes(nounce);
220 
221             nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp());
222         }
223         while (_nonceCount.putIfAbsent(nonce._nonce,nonce)!=null);
224         _nonceQueue.add(nonce);
225                
226         return nonce._nonce;
227     }
228 
229     /**
230      * @param nstring nonce to check
231      * @param request
232      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
233      */
234     /* ------------------------------------------------------------ */
235     private int checkNonce(Digest digest, Request request)
236     {
237         // firstly let's expire old nonces
238         long expired = request.getTimeStamp()-_maxNonceAgeMs;
239         
240         Nonce nonce=_nonceQueue.peek();
241         while (nonce!=null && nonce._ts<expired)
242         {
243             _nonceQueue.remove();
244             _nonceCount.remove(nonce._nonce);
245             nonce=_nonceQueue.peek();
246         }
247         
248        
249         try
250         {
251             nonce = _nonceCount.get(digest.nonce);
252             if (nonce==null)
253                 return 0;
254             
255             long count = Long.parseLong(digest.nc,16);
256             if (count>Integer.MAX_VALUE)
257                 return 0;
258             int old=nonce._nc.get();
259             while (!nonce._nc.compareAndSet(old,(int)count))
260                 old=nonce._nc.get();
261             if (count<=old)
262                 return -1;
263  
264             return 1;
265         }
266         catch (Exception e)
267         {
268             LOG.ignore(e);
269         }
270         return -1;
271     }
272 
273     /* ------------------------------------------------------------ */
274     /* ------------------------------------------------------------ */
275     /* ------------------------------------------------------------ */
276     private static class Digest extends Credential
277     {
278         private static final long serialVersionUID = -2484639019549527724L;
279         final String method;
280         String username = "";
281         String realm = "";
282         String nonce = "";
283         String nc = "";
284         String cnonce = "";
285         String qop = "";
286         String uri = "";
287         String response = "";
288 
289         /* ------------------------------------------------------------ */
290         Digest(String m)
291         {
292             method = m;
293         }
294 
295         /* ------------------------------------------------------------ */
296         @Override
297         public boolean check(Object credentials)
298         {
299             if (credentials instanceof char[])
300                 credentials=new String((char[])credentials);
301             String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
302 
303             try
304             {
305                 MessageDigest md = MessageDigest.getInstance("MD5");
306                 byte[] ha1;
307                 if (credentials instanceof Credential.MD5)
308                 {
309                     // Credentials are already a MD5 digest - assume it's in
310                     // form user:realm:password (we have no way to know since
311                     // it's a digest, alright?)
312                     ha1 = ((Credential.MD5) credentials).getDigest();
313                 }
314                 else
315                 {
316                     // calc A1 digest
317                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
318                     md.update((byte) ':');
319                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
320                     md.update((byte) ':');
321                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
322                     ha1 = md.digest();
323                 }
324                 // calc A2 digest
325                 md.reset();
326                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
327                 md.update((byte) ':');
328                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
329                 byte[] ha2 = md.digest();
330 
331                 // calc digest
332                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
333                 // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
334                 // <">
335                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
336                 // ) > <">
337 
338                 md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1));
339                 md.update((byte) ':');
340                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
341                 md.update((byte) ':');
342                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
343                 md.update((byte) ':');
344                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
345                 md.update((byte) ':');
346                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
347                 md.update((byte) ':');
348                 md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1));
349                 byte[] digest = md.digest();
350 
351                 // check digest
352                 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
353             }
354             catch (Exception e)
355             {
356                 LOG.warn(e);
357             }
358 
359             return false;
360         }
361 
362         public String toString()
363         {
364             return username + "," + response;
365         }
366     }
367 }