1
2
3
4
5
6
7
8
9
10
11
12
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
33
34
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
46
47
48
49
50 public Principal authenticate(UserRealm realm,
51 String pathInContext,
52 Request request,
53 Response response)
54 throws IOException
55 {
56
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
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
201
202
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;
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
290
291
292 ha1 = ((Credential.MD5)credentials).getDigest();
293 }
294 else
295 {
296
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
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
316
317
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
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
351
352 public long getMaxNonceAge()
353 {
354 return maxNonceAge;
355 }
356
357
358
359 public void setMaxNonceAge(long maxNonceAge)
360 {
361 this.maxNonceAge = maxNonceAge;
362 }
363
364
365
366 public long getNonceSecret()
367 {
368 return nonceSecret;
369 }
370
371
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