1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.security.authentication;
20
21 import java.io.IOException;
22 import java.security.MessageDigest;
23 import java.security.SecureRandom;
24 import java.util.BitSet;
25 import java.util.Queue;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ConcurrentLinkedQueue;
28 import java.util.concurrent.ConcurrentMap;
29 import java.util.concurrent.atomic.AtomicInteger;
30
31 import javax.servlet.ServletRequest;
32 import javax.servlet.ServletResponse;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
35
36 import org.eclipse.jetty.http.HttpHeaders;
37 import org.eclipse.jetty.security.SecurityHandler;
38 import org.eclipse.jetty.security.ServerAuthException;
39 import org.eclipse.jetty.security.UserAuthentication;
40 import org.eclipse.jetty.server.Authentication;
41 import org.eclipse.jetty.server.Authentication.User;
42 import org.eclipse.jetty.server.Request;
43 import org.eclipse.jetty.server.UserIdentity;
44 import org.eclipse.jetty.util.B64Code;
45 import org.eclipse.jetty.util.QuotedStringTokenizer;
46 import org.eclipse.jetty.util.StringUtil;
47 import org.eclipse.jetty.util.TypeUtil;
48 import org.eclipse.jetty.util.log.Log;
49 import org.eclipse.jetty.util.log.Logger;
50 import org.eclipse.jetty.util.security.Constraint;
51 import org.eclipse.jetty.util.security.Credential;
52
53
54
55
56
57
58
59 public class DigestAuthenticator extends LoginAuthenticator
60 {
61 private static final Logger LOG = Log.getLogger(DigestAuthenticator.class);
62 SecureRandom _random = new SecureRandom();
63 private long _maxNonceAgeMs = 60*1000;
64 private int _maxNC=1024;
65 private ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<String, Nonce>();
66 private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<Nonce>();
67 private static class Nonce
68 {
69 final String _nonce;
70 final long _ts;
71 final BitSet _seen;
72
73 public Nonce(String nonce, long ts, int size)
74 {
75 _nonce=nonce;
76 _ts=ts;
77 _seen = new BitSet(size);
78 }
79
80 public boolean seen(int count)
81 {
82 synchronized (this)
83 {
84 if (count>=_seen.size())
85 return true;
86 boolean s=_seen.get(count);
87 _seen.set(count);
88 return s;
89 }
90 }
91 }
92
93
94 public DigestAuthenticator()
95 {
96 super();
97 }
98
99
100
101
102
103 @Override
104 public void setConfiguration(AuthConfiguration configuration)
105 {
106 super.setConfiguration(configuration);
107
108 String mna=configuration.getInitParameter("maxNonceAge");
109 if (mna!=null)
110 {
111 _maxNonceAgeMs=Long.valueOf(mna);
112 }
113 }
114
115
116
117 public int getMaxNonceCount()
118 {
119 return _maxNC;
120 }
121
122
123 public void setMaxNonceCount(int maxNC)
124 {
125 _maxNC = maxNC;
126 }
127
128
129 public void setMaxNonceAge(long maxNonceAgeInMillis)
130 {
131 _maxNonceAgeMs = maxNonceAgeInMillis;
132 }
133
134
135 public long getMaxNonceAge()
136 {
137 return _maxNonceAgeMs;
138 }
139
140
141 public String getAuthMethod()
142 {
143 return Constraint.__DIGEST_AUTH;
144 }
145
146
147 public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
148 {
149 return true;
150 }
151
152
153
154
155 public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
156 {
157 if (!mandatory)
158 return new DeferredAuthentication(this);
159
160 HttpServletRequest request = (HttpServletRequest)req;
161 HttpServletResponse response = (HttpServletResponse)res;
162 String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
163
164 try
165 {
166 boolean stale = false;
167 if (credentials != null)
168 {
169 if (LOG.isDebugEnabled())
170 LOG.debug("Credentials: " + credentials);
171 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
172 final Digest digest = new Digest(request.getMethod());
173 String last = null;
174 String name = null;
175
176 while (tokenizer.hasMoreTokens())
177 {
178 String tok = tokenizer.nextToken();
179 char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
180
181 switch (c)
182 {
183 case '=':
184 name = last;
185 last = tok;
186 break;
187 case ',':
188 name = null;
189 break;
190 case ' ':
191 break;
192
193 default:
194 last = tok;
195 if (name != null)
196 {
197 if ("username".equalsIgnoreCase(name))
198 digest.username = tok;
199 else if ("realm".equalsIgnoreCase(name))
200 digest.realm = tok;
201 else if ("nonce".equalsIgnoreCase(name))
202 digest.nonce = tok;
203 else if ("nc".equalsIgnoreCase(name))
204 digest.nc = tok;
205 else if ("cnonce".equalsIgnoreCase(name))
206 digest.cnonce = tok;
207 else if ("qop".equalsIgnoreCase(name))
208 digest.qop = tok;
209 else if ("uri".equalsIgnoreCase(name))
210 digest.uri = tok;
211 else if ("response".equalsIgnoreCase(name))
212 digest.response = tok;
213 name=null;
214 }
215 }
216 }
217
218 int n = checkNonce(digest,(Request)request);
219
220 if (n > 0)
221 {
222
223 UserIdentity user = login(digest.username, digest, req);
224 if (user!=null)
225 {
226 return new UserAuthentication(getAuthMethod(),user);
227 }
228 }
229 else if (n == 0)
230 stale = true;
231
232 }
233
234 if (!DeferredAuthentication.isDeferred(response))
235 {
236 String domain = request.getContextPath();
237 if (domain == null)
238 domain = "/";
239 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + _loginService.getName()
240 + "\", domain=\""
241 + domain
242 + "\", nonce=\""
243 + newNonce((Request)request)
244 + "\", algorithm=MD5, qop=\"auth\","
245 + " stale=" + stale);
246 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
247
248 return Authentication.SEND_CONTINUE;
249 }
250
251 return Authentication.UNAUTHENTICATED;
252 }
253 catch (IOException e)
254 {
255 throw new ServerAuthException(e);
256 }
257
258 }
259
260
261 public String newNonce(Request request)
262 {
263 Nonce nonce;
264
265 do
266 {
267 byte[] nounce = new byte[24];
268 _random.nextBytes(nounce);
269
270 nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp(),_maxNC);
271 }
272 while (_nonceMap.putIfAbsent(nonce._nonce,nonce)!=null);
273 _nonceQueue.add(nonce);
274
275 return nonce._nonce;
276 }
277
278
279
280
281
282
283
284 private int checkNonce(Digest digest, Request request)
285 {
286
287 long expired = request.getTimeStamp()-_maxNonceAgeMs;
288 Nonce nonce=_nonceQueue.peek();
289 while (nonce!=null && nonce._ts<expired)
290 {
291 _nonceQueue.remove(nonce);
292 _nonceMap.remove(nonce._nonce);
293 nonce=_nonceQueue.peek();
294 }
295
296
297 try
298 {
299 nonce = _nonceMap.get(digest.nonce);
300 if (nonce==null)
301 return 0;
302
303 long count = Long.parseLong(digest.nc,16);
304 if (count>=_maxNC)
305 return 0;
306 if (nonce.seen((int)count))
307 return -1;
308 return 1;
309 }
310 catch (Exception e)
311 {
312 LOG.ignore(e);
313 }
314 return -1;
315 }
316
317
318
319
320 private static class Digest extends Credential
321 {
322 private static final long serialVersionUID = -2484639019549527724L;
323 final String method;
324 String username = "";
325 String realm = "";
326 String nonce = "";
327 String nc = "";
328 String cnonce = "";
329 String qop = "";
330 String uri = "";
331 String response = "";
332
333
334 Digest(String m)
335 {
336 method = m;
337 }
338
339
340 @Override
341 public boolean check(Object credentials)
342 {
343 if (credentials instanceof char[])
344 credentials=new String((char[])credentials);
345 String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
346
347 try
348 {
349 MessageDigest md = MessageDigest.getInstance("MD5");
350 byte[] ha1;
351 if (credentials instanceof Credential.MD5)
352 {
353
354
355
356 ha1 = ((Credential.MD5) credentials).getDigest();
357 }
358 else
359 {
360
361 md.update(username.getBytes(StringUtil.__ISO_8859_1));
362 md.update((byte) ':');
363 md.update(realm.getBytes(StringUtil.__ISO_8859_1));
364 md.update((byte) ':');
365 md.update(password.getBytes(StringUtil.__ISO_8859_1));
366 ha1 = md.digest();
367 }
368
369 md.reset();
370 md.update(method.getBytes(StringUtil.__ISO_8859_1));
371 md.update((byte) ':');
372 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
373 byte[] ha2 = md.digest();
374
375
376
377
378
379
380
381
382 md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1));
383 md.update((byte) ':');
384 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
385 md.update((byte) ':');
386 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
387 md.update((byte) ':');
388 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
389 md.update((byte) ':');
390 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
391 md.update((byte) ':');
392 md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1));
393 byte[] digest = md.digest();
394
395
396 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
397 }
398 catch (Exception e)
399 {
400 LOG.warn(e);
401 }
402
403 return false;
404 }
405
406 @Override
407 public String toString()
408 {
409 return username + "," + response;
410 }
411 }
412 }