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      */
authenticate(UserRealm realm, String pathInContext, Request request, Response response)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                           break;
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     /* ------------------------------------------------------------ */
getAuthMethod()137     public String getAuthMethod()
138     {
139         return Constraint.__DIGEST_AUTH;
140     }
141 
142     /* ------------------------------------------------------------ */
sendChallenge(UserRealm realm, Request request, Response response, boolean stale)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     /* ------------------------------------------------------------ */
newNonce(Request request)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     /* ------------------------------------------------------------ */
checkNonce(String nonce, Request request)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         /* ------------------------------------------------------------ */
Digest(String m)272         Digest(String m)
273         {
274             method=m;
275         }
276 
277         /* ------------------------------------------------------------ */
check(Object credentials)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 
toString()343         public String toString()
344         {
345             return username+","+response;
346         }
347 
348     }
349     /**
350      * @return Returns the maxNonceAge.
351      */
getMaxNonceAge()352     public long getMaxNonceAge()
353     {
354         return maxNonceAge;
355     }
356     /**
357      * @param maxNonceAge The maxNonceAge to set.
358      */
setMaxNonceAge(long maxNonceAge)359     public void setMaxNonceAge(long maxNonceAge)
360     {
361         this.maxNonceAge = maxNonceAge;
362     }
363     /**
364      * @return Returns the nonceSecret.
365      */
getNonceSecret()366     public long getNonceSecret()
367     {
368         return nonceSecret;
369     }
370     /**
371      * @param nonceSecret The nonceSecret to set.
372      */
setNonceSecret(long nonceSecret)373     public void setNonceSecret(long nonceSecret)
374     {
375         this.nonceSecret = nonceSecret;
376     }
377 
setUseStale(boolean us)378     public void setUseStale(boolean us)
379     {
380 	this.useStale=us;
381     }
382 
getUseStale()383     public boolean getUseStale()
384     {
385 	return useStale;
386     }
387 }
388 
389