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