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