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