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  
19  import javax.servlet.ServletRequest;
20  import javax.servlet.ServletResponse;
21  import javax.servlet.http.HttpServletRequest;
22  import javax.servlet.http.HttpServletResponse;
23  
24  import org.eclipse.jetty.http.HttpHeaders;
25  import org.eclipse.jetty.http.security.B64Code;
26  import org.eclipse.jetty.http.security.Constraint;
27  import org.eclipse.jetty.http.security.Credential;
28  import org.eclipse.jetty.security.ServerAuthException;
29  import org.eclipse.jetty.security.UserAuthentication;
30  import org.eclipse.jetty.server.Authentication;
31  import org.eclipse.jetty.server.Request;
32  import org.eclipse.jetty.server.UserIdentity;
33  import org.eclipse.jetty.server.Authentication.User;
34  import org.eclipse.jetty.util.QuotedStringTokenizer;
35  import org.eclipse.jetty.util.StringUtil;
36  import org.eclipse.jetty.util.TypeUtil;
37  import org.eclipse.jetty.util.log.Log;
38  
39  /**
40   * @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
41   */
42  public class DigestAuthenticator extends LoginAuthenticator
43  {
44      protected long _maxNonceAge = 0;
45      protected long _nonceSecret = this.hashCode() ^ System.currentTimeMillis();
46      protected boolean _useStale = false;
47  
48      public DigestAuthenticator()
49      {
50          super();
51      }
52  
53      public String getAuthMethod()
54      {
55          return Constraint.__DIGEST_AUTH;
56      }
57      
58      public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
59      {
60          return true;
61      }
62  
63      public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
64      {
65          if (!mandatory)
66              return _deferred;
67          
68          HttpServletRequest request = (HttpServletRequest)req;
69          HttpServletResponse response = (HttpServletResponse)res;
70          String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
71  
72          try
73          {
74              boolean stale = false;
75              if (credentials != null)
76              {
77                  if (Log.isDebugEnabled()) Log.debug("Credentials: " + credentials);
78                  QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
79                  final Digest digest = new Digest(request.getMethod());
80                  String last = null;
81                  String name = null;
82  
83                  while (tokenizer.hasMoreTokens())
84                  {
85                      String tok = tokenizer.nextToken();
86                      char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
87  
88                      switch (c)
89                      {
90                          case '=':
91                              name = last;
92                              last = tok;
93                              break;
94                          case ',':
95                              name = null;
96                          case ' ':
97                              break;
98  
99                          default:
100                             last = tok;
101                             if (name != null)
102                             {
103                                 if ("username".equalsIgnoreCase(name))
104                                     digest.username = tok;
105                                 else if ("realm".equalsIgnoreCase(name))
106                                     digest.realm = tok;
107                                 else if ("nonce".equalsIgnoreCase(name))
108                                     digest.nonce = tok;
109                                 else if ("nc".equalsIgnoreCase(name))
110                                     digest.nc = tok;
111                                 else if ("cnonce".equalsIgnoreCase(name))
112                                     digest.cnonce = tok;
113                                 else if ("qop".equalsIgnoreCase(name))
114                                     digest.qop = tok;
115                                 else if ("uri".equalsIgnoreCase(name))
116                                     digest.uri = tok;
117                                 else if ("response".equalsIgnoreCase(name)) 
118                                     digest.response = tok;
119                                 name=null;
120                             }
121                     }
122                 }
123 
124                 int n = checkNonce(digest.nonce, (Request)request);
125 
126                 if (n > 0)
127                 {
128                     UserIdentity user = _loginService.login(digest.username,digest);
129                     if (user!=null)
130                         return new UserAuthentication(this,user);
131                 }
132                 else if (n == 0) 
133                     stale = true;
134 
135             }
136 
137             if (!_deferred.isDeferred(response))
138             {
139                 String domain = request.getContextPath();
140                 if (domain == null) 
141                     domain = "/";
142                 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName()
143                         + "\", domain=\""
144                         + domain
145                         + "\", nonce=\""
146                         + newNonce((Request)request)
147                         + "\", algorithm=MD5, qop=\"auth\""
148                         + (_useStale ? (" stale=" + stale) : ""));
149                 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
150 
151                 return Authentication.SEND_CONTINUE;
152             }
153 
154             return Authentication.UNAUTHENTICATED;
155         }
156         catch (IOException e)
157         {
158             throw new ServerAuthException(e);
159         }
160 
161     }
162 
163     public String newNonce(Request request)
164     {
165         long ts=request.getTimeStamp();
166         long sk = _nonceSecret;
167 
168         byte[] nounce = new byte[24];
169         for (int i = 0; i < 8; i++)
170         {
171             nounce[i] = (byte) (ts & 0xff);
172             ts = ts >> 8;
173             nounce[8 + i] = (byte) (sk & 0xff);
174             sk = sk >> 8;
175         }
176 
177         byte[] hash = null;
178         try
179         {
180             MessageDigest md = MessageDigest.getInstance("MD5");
181             md.reset();
182             md.update(nounce, 0, 16);
183             hash = md.digest();
184         }
185         catch (Exception e)
186         {
187             Log.warn(e);
188         }
189 
190         for (int i = 0; i < hash.length; i++)
191         {
192             nounce[8 + i] = hash[i];
193             if (i == 23) break;
194         }
195 
196         return new String(B64Code.encode(nounce));
197     }
198 
199     /**
200      * @param nonce nonce to check
201      * @param request
202      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
203      */
204     /* ------------------------------------------------------------ */
205     private int checkNonce(String nonce, Request request)
206     {
207         try
208         {
209             byte[] n = B64Code.decode(nonce.toCharArray());
210             if (n.length != 24) return -1;
211 
212             long ts = 0;
213             long sk = _nonceSecret;
214             byte[] n2 = new byte[16];
215             System.arraycopy(n, 0, n2, 0, 8);
216             for (int i = 0; i < 8; i++)
217             {
218                 n2[8 + i] = (byte) (sk & 0xff);
219                 sk = sk >> 8;
220                 ts = (ts << 8) + (0xff & (long) n[7 - i]);
221             }
222 
223             long age = request.getTimeStamp() - ts;
224             if (Log.isDebugEnabled()) Log.debug("age=" + age);
225 
226             byte[] hash = null;
227             try
228             {
229                 MessageDigest md = MessageDigest.getInstance("MD5");
230                 md.reset();
231                 md.update(n2, 0, 16);
232                 hash = md.digest();
233             }
234             catch (Exception e)
235             {
236                 Log.warn(e);
237             }
238 
239             for (int i = 0; i < 16; i++)
240                 if (n[i + 8] != hash[i]) return -1;
241 
242             if (_maxNonceAge > 0 && (age < 0 || age > _maxNonceAge)) return 0; // stale
243 
244             return 1;
245         }
246         catch (Exception e)
247         {
248             Log.ignore(e);
249         }
250         return -1;
251     }
252 
253     private static class Digest extends Credential
254     {
255         String method = null;
256         String username = null;
257         String realm = null;
258         String nonce = null;
259         String nc = null;
260         String cnonce = null;
261         String qop = null;
262         String uri = null;
263         String response = null;
264 
265         /* ------------------------------------------------------------ */
266         Digest(String m)
267         {
268             method = m;
269         }
270 
271         /* ------------------------------------------------------------ */
272         public boolean check(Object credentials)
273         {
274             if (credentials instanceof char[])
275                 credentials=new String((char[])credentials);
276             String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
277 
278             try
279             {
280                 MessageDigest md = MessageDigest.getInstance("MD5");
281                 byte[] ha1;
282                 if (credentials instanceof Credential.MD5)
283                 {
284                     // Credentials are already a MD5 digest - assume it's in
285                     // form user:realm:password (we have no way to know since
286                     // it's a digest, alright?)
287                     ha1 = ((Credential.MD5) credentials).getDigest();
288                 }
289                 else
290                 {
291                     // calc A1 digest
292                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
293                     md.update((byte) ':');
294                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
295                     md.update((byte) ':');
296                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
297                     ha1 = md.digest();
298                 }
299                 // calc A2 digest
300                 md.reset();
301                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
302                 md.update((byte) ':');
303                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
304                 byte[] ha2 = md.digest();
305 
306                 // calc digest
307                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":"
308                 // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )
309                 // <">
310                 // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2)
311                 // ) > <">
312 
313                 md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1));
314                 md.update((byte) ':');
315                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
316                 md.update((byte) ':');
317                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
318                 md.update((byte) ':');
319                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
320                 md.update((byte) ':');
321                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
322                 md.update((byte) ':');
323                 md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1));
324                 byte[] digest = md.digest();
325 
326                 // check digest
327                 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
328             }
329             catch (Exception e)
330             {
331                 Log.warn(e);
332             }
333 
334             return false;
335         }
336 
337         public String toString()
338         {
339             return username + "," + response;
340         }
341     }
342 }