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