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