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