View Javadoc

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