View Javadoc

1   // ========================================================================
2   // Copyright 2002-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty.security;
16  
17  import java.io.IOException;
18  import java.security.MessageDigest;
19  import java.security.Principal;
20  
21  import javax.servlet.http.HttpServletResponse;
22  
23  import org.mortbay.jetty.HttpHeaders;
24  import org.mortbay.jetty.Request;
25  import org.mortbay.jetty.Response;
26  import org.mortbay.log.Log;
27  import org.mortbay.util.QuotedStringTokenizer;
28  import org.mortbay.util.StringUtil;
29  import org.mortbay.util.TypeUtil;
30  
31  /* ------------------------------------------------------------ */
32  /** DIGEST authentication.
33   *
34   * @author Greg Wilkins (gregw)
35   */
36  public class DigestAuthenticator implements Authenticator
37  {
38      protected long maxNonceAge=0;
39      protected long nonceSecret=this.hashCode() ^ System.currentTimeMillis();
40      protected boolean useStale=false;
41      
42      
43      /* ------------------------------------------------------------ */
44      /** 
45       * @return UserPrinciple if authenticated or null if not. If
46       * Authentication fails, then the authenticator may have committed
47       * the response as an auth challenge or redirect.
48       * @exception IOException 
49       */
50      public Principal authenticate(UserRealm realm,
51                                             String pathInContext,
52                                             Request request,
53                                             Response response)
54          throws IOException
55      {
56          // Get the user if we can
57          boolean stale=false;
58          Principal user=null;
59          String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
60          
61          if (credentials!=null )
62          {
63              if(Log.isDebugEnabled())Log.debug("Credentials: "+credentials);
64              QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials,
65                                                                          "=, ",
66                                                                          true,
67                                                                          false);
68              Digest digest=new Digest(request.getMethod());
69              String last=null;
70              String name=null;
71  
72            loop:
73              while (tokenizer.hasMoreTokens())
74              {
75                  String tok = tokenizer.nextToken();
76                  char c=(tok.length()==1)?tok.charAt(0):'\0';
77  
78                  switch (c)
79                  {
80                    case '=':
81                        name=last;
82                        last=tok;
83                        break;
84                    case ',':
85                        name=null;
86                    case ' ':
87                        break;
88  
89                    default:
90                        last=tok;
91                        if (name!=null)
92                        {
93                            if ("username".equalsIgnoreCase(name))
94                                digest.username=tok;
95                            else if ("realm".equalsIgnoreCase(name))
96                                digest.realm=tok;
97                            else if ("nonce".equalsIgnoreCase(name))
98                                digest.nonce=tok;
99                            else if ("nc".equalsIgnoreCase(name))
100                               digest.nc=tok;
101                           else if ("cnonce".equalsIgnoreCase(name))
102                               digest.cnonce=tok;
103                           else if ("qop".equalsIgnoreCase(name))
104                               digest.qop=tok;
105                           else if ("uri".equalsIgnoreCase(name))
106                               digest.uri=tok;
107                           else if ("response".equalsIgnoreCase(name))
108                               digest.response=tok;
109                           name=null;
110                       }
111                 }
112             }            
113 
114             int n=checkNonce(digest.nonce,request);
115             if (n>0)
116                 user = realm.authenticate(digest.username,digest,request);
117             else if (n==0)
118                 stale = true;
119             
120             if (user==null)
121                 Log.warn("AUTH FAILURE: user "+StringUtil.printable(digest.username));
122             else    
123             {
124                 request.setAuthType(Constraint.__DIGEST_AUTH);
125                 request.setUserPrincipal(user);                
126             }
127         }
128 
129         // Challenge if we have no user
130         if (user==null && response!=null)
131             sendChallenge(realm,request,response,stale);
132         
133         return user;
134     }
135     
136     /* ------------------------------------------------------------ */
137     public String getAuthMethod()
138     {
139         return Constraint.__DIGEST_AUTH;
140     }
141     
142     /* ------------------------------------------------------------ */
143     public void sendChallenge(UserRealm realm,
144                               Request request,
145                               Response response,
146                               boolean stale)
147         throws IOException
148     {
149         String domain=request.getContextPath();
150         if (domain==null)
151             domain="/";
152         response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
153 			    "Digest realm=\""+realm.getName()+
154 			    "\", domain=\""+domain +
155 			    "\", nonce=\""+newNonce(request)+
156 			    "\", algorithm=MD5, qop=\"auth\"" + (useStale?(" stale="+stale):"")
157                           );
158         response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
159     }
160 
161     /* ------------------------------------------------------------ */
162     public String newNonce(Request request)
163     {
164         long ts=request.getTimeStamp();
165         long sk=nonceSecret;
166         
167         byte[] nounce = new byte[24];
168         for (int i=0;i<8;i++)
169         {
170             nounce[i]=(byte)(ts&0xff);
171             ts=ts>>8;
172             nounce[8+i]=(byte)(sk&0xff);
173             sk=sk>>8;
174         }
175         
176         byte[] hash=null;
177         try
178         {
179             MessageDigest md = MessageDigest.getInstance("MD5");
180             md.reset();
181             md.update(nounce,0,16);
182             hash = md.digest();
183         }
184         catch(Exception e)
185         {
186             Log.warn(e);
187         }
188         
189         for (int i=0;i<hash.length;i++)
190         {
191             nounce[8+i]=hash[i];
192             if (i==23)
193                 break;
194         }
195         
196         return new String(B64Code.encode(nounce));
197     }
198 
199     /**
200      * @param nonce
201      * @param request
202      * @return -1 for a bad nonce, 0 for a stale none, 1 for a good nonce
203      */
204     /* ------------------------------------------------------------ */
205     public int checkNonce(String nonce, Request request)
206     {
207         try
208         {
209             byte[] n = B64Code.decode(nonce.toCharArray());
210             if (n.length!=24)
211                 return -1;
212             
213             long ts=0;
214             long sk=nonceSecret;
215             byte[] n2 = new byte[16];
216             System.arraycopy(n, 0, n2, 0, 8);
217             for (int i=0;i<8;i++)
218             {
219                 n2[8+i]=(byte)(sk&0xff);
220                 sk=sk>>8;
221                 ts=(ts<<8)+(0xff&(long)n[7-i]);
222             }
223             
224             long age=request.getTimeStamp()-ts;
225             if (Log.isDebugEnabled()) Log.debug("age="+age);
226             
227             byte[] hash=null;
228             try
229             {
230                 MessageDigest md = MessageDigest.getInstance("MD5");
231                 md.reset();
232                 md.update(n2,0,16);
233                 hash = md.digest();
234             }
235             catch(Exception e)
236             {
237                 Log.warn(e);
238             }
239             
240             for (int i=0;i<16;i++)
241                 if (n[i+8]!=hash[i])
242                     return -1;
243                 
244             if(maxNonceAge>0 && (age<0 || age>maxNonceAge))
245                 return 0; // stale
246             
247             return 1;
248         }
249         catch(Exception e)
250         {
251             Log.ignore(e);
252         }
253         return -1;
254     }
255 
256     /* ------------------------------------------------------------ */
257     /* ------------------------------------------------------------ */
258     /* ------------------------------------------------------------ */
259     private static class Digest extends Credential
260     {
261         String method=null;
262         String username = null;
263         String realm = null;
264         String nonce = null;
265         String nc = null;
266         String cnonce = null;
267         String qop = null;
268         String uri = null;
269         String response=null;
270         
271         /* ------------------------------------------------------------ */
272         Digest(String m)
273         {
274             method=m;
275         }
276         
277         /* ------------------------------------------------------------ */
278         public boolean check(Object credentials)
279         {
280             String password=(credentials instanceof String)
281                 ?(String)credentials
282                 :credentials.toString();
283             
284             try{
285                 MessageDigest md = MessageDigest.getInstance("MD5");
286                 byte[] ha1;
287                 if(credentials instanceof Credential.MD5)
288                 {
289                     // Credentials are already a MD5 digest - assume it's in
290                     // form user:realm:password (we have no way to know since 
291                     // it's a digest, alright?)
292                     ha1 = ((Credential.MD5)credentials).getDigest();
293                 }
294                 else
295                 {
296                     // calc A1 digest
297                     md.update(username.getBytes(StringUtil.__ISO_8859_1));
298                     md.update((byte)':');
299                     md.update(realm.getBytes(StringUtil.__ISO_8859_1));
300                     md.update((byte)':');
301                     md.update(password.getBytes(StringUtil.__ISO_8859_1));
302                     ha1=md.digest();
303                 }
304                 // calc A2 digest
305                 md.reset();
306                 md.update(method.getBytes(StringUtil.__ISO_8859_1));
307                 md.update((byte)':');
308                 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
309                 byte[] ha2=md.digest();
310                 
311                 
312                 
313                 
314                 
315                 // calc digest
316                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) <">
317                 // request-digest  = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
318 
319                 
320                 
321                 md.update(TypeUtil.toString(ha1,16).getBytes(StringUtil.__ISO_8859_1));
322                 md.update((byte)':');
323                 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
324                 md.update((byte)':');
325                 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
326                 md.update((byte)':');
327                 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
328                 md.update((byte)':');
329                 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
330                 md.update((byte)':');
331                 md.update(TypeUtil.toString(ha2,16).getBytes(StringUtil.__ISO_8859_1));
332                 byte[] digest=md.digest();
333                 
334                 // check digest
335                 return (TypeUtil.toString(digest,16).equalsIgnoreCase(response));
336             }
337             catch (Exception e)
338             {Log.warn(e);}
339 
340             return false;
341         }
342 
343         public String toString()
344         {
345             return username+","+response;
346         }
347         
348     }
349     /**
350      * @return Returns the maxNonceAge.
351      */
352     public long getMaxNonceAge()
353     {
354         return maxNonceAge;
355     }
356     /**
357      * @param maxNonceAge The maxNonceAge to set.
358      */
359     public void setMaxNonceAge(long maxNonceAge)
360     {
361         this.maxNonceAge = maxNonceAge;
362     }
363     /**
364      * @return Returns the nonceSecret.
365      */
366     public long getNonceSecret()
367     {
368         return nonceSecret;
369     }
370     /**
371      * @param nonceSecret The nonceSecret to set.
372      */
373     public void setNonceSecret(long nonceSecret)
374     {
375         this.nonceSecret = nonceSecret;
376     }
377 
378     public void setUseStale(boolean us)
379     {
380 	this.useStale=us;
381     }
382 
383     public boolean getUseStale()
384     {
385 	return useStale;
386     }
387 }
388