View Javadoc

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