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.nio.charset.StandardCharsets;
23 import java.security.MessageDigest;
24 import java.security.SecureRandom;
25 import java.util.BitSet;
26 import java.util.Queue;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ConcurrentLinkedQueue;
29 import java.util.concurrent.ConcurrentMap;
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.HttpHeader;
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.TypeUtil;
47 import org.eclipse.jetty.util.log.Log;
48 import org.eclipse.jetty.util.log.Logger;
49 import org.eclipse.jetty.util.security.Constraint;
50 import org.eclipse.jetty.util.security.Credential;
51
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 String mnc=configuration.getInitParameter("maxNonceCount");
114 if (mnc!=null)
115 {
116 _maxNC=Integer.valueOf(mnc);
117 }
118 }
119
120
121 public int getMaxNonceCount()
122 {
123 return _maxNC;
124 }
125
126
127 public void setMaxNonceCount(int maxNC)
128 {
129 _maxNC = maxNC;
130 }
131
132
133 public long getMaxNonceAge()
134 {
135 return _maxNonceAgeMs;
136 }
137
138
139 public synchronized void setMaxNonceAge(long maxNonceAgeInMillis)
140 {
141 _maxNonceAgeMs = maxNonceAgeInMillis;
142 }
143
144
145 @Override
146 public String getAuthMethod()
147 {
148 return Constraint.__DIGEST_AUTH;
149 }
150
151
152 @Override
153 public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, User validatedUser) throws ServerAuthException
154 {
155 return true;
156 }
157
158
159
160
161 @Override
162 public Authentication validateRequest(ServletRequest req, ServletResponse res, boolean mandatory) throws ServerAuthException
163 {
164 if (!mandatory)
165 return new DeferredAuthentication(this);
166
167 HttpServletRequest request = (HttpServletRequest)req;
168 HttpServletResponse response = (HttpServletResponse)res;
169 String credentials = request.getHeader(HttpHeader.AUTHORIZATION.asString());
170
171 try
172 {
173 boolean stale = false;
174 if (credentials != null)
175 {
176 if (LOG.isDebugEnabled())
177 LOG.debug("Credentials: " + credentials);
178 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
179 final Digest digest = new Digest(request.getMethod());
180 String last = null;
181 String name = null;
182
183 while (tokenizer.hasMoreTokens())
184 {
185 String tok = tokenizer.nextToken();
186 char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
187
188 switch (c)
189 {
190 case '=':
191 name = last;
192 last = tok;
193 break;
194 case ',':
195 name = null;
196 break;
197 case ' ':
198 break;
199
200 default:
201 last = tok;
202 if (name != null)
203 {
204 if ("username".equalsIgnoreCase(name))
205 digest.username = tok;
206 else if ("realm".equalsIgnoreCase(name))
207 digest.realm = tok;
208 else if ("nonce".equalsIgnoreCase(name))
209 digest.nonce = tok;
210 else if ("nc".equalsIgnoreCase(name))
211 digest.nc = tok;
212 else if ("cnonce".equalsIgnoreCase(name))
213 digest.cnonce = tok;
214 else if ("qop".equalsIgnoreCase(name))
215 digest.qop = tok;
216 else if ("uri".equalsIgnoreCase(name))
217 digest.uri = tok;
218 else if ("response".equalsIgnoreCase(name))
219 digest.response = tok;
220 name=null;
221 }
222 }
223 }
224
225 int n = checkNonce(digest,(Request)request);
226
227 if (n > 0)
228 {
229
230 UserIdentity user = login(digest.username, digest, req);
231 if (user!=null)
232 {
233 return new UserAuthentication(getAuthMethod(),user);
234 }
235 }
236 else if (n == 0)
237 stale = true;
238
239 }
240
241 if (!DeferredAuthentication.isDeferred(response))
242 {
243 String domain = request.getContextPath();
244 if (domain == null)
245 domain = "/";
246 response.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Digest realm=\"" + _loginService.getName()
247 + "\", domain=\""
248 + domain
249 + "\", nonce=\""
250 + newNonce((Request)request)
251 + "\", algorithm=MD5, qop=\"auth\","
252 + " stale=" + stale);
253 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
254
255 return Authentication.SEND_CONTINUE;
256 }
257
258 return Authentication.UNAUTHENTICATED;
259 }
260 catch (IOException e)
261 {
262 throw new ServerAuthException(e);
263 }
264
265 }
266
267
268 public String newNonce(Request request)
269 {
270 Nonce nonce;
271
272 do
273 {
274 byte[] nounce = new byte[24];
275 _random.nextBytes(nounce);
276
277 nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp(),_maxNC);
278 }
279 while (_nonceMap.putIfAbsent(nonce._nonce,nonce)!=null);
280 _nonceQueue.add(nonce);
281
282 return nonce._nonce;
283 }
284
285
286
287
288
289
290
291 private int checkNonce(Digest digest, Request request)
292 {
293
294 long expired = request.getTimeStamp()-_maxNonceAgeMs;
295 Nonce nonce=_nonceQueue.peek();
296 while (nonce!=null && nonce._ts<expired)
297 {
298 _nonceQueue.remove(nonce);
299 _nonceMap.remove(nonce._nonce);
300 nonce=_nonceQueue.peek();
301 }
302
303
304 try
305 {
306 nonce = _nonceMap.get(digest.nonce);
307 if (nonce==null)
308 return 0;
309
310 long count = Long.parseLong(digest.nc,16);
311 if (count>=_maxNC)
312 return 0;
313
314 if (nonce.seen((int)count))
315 return -1;
316
317 return 1;
318 }
319 catch (Exception e)
320 {
321 LOG.ignore(e);
322 }
323 return -1;
324 }
325
326
327
328
329 private static class Digest extends Credential
330 {
331 private static final long serialVersionUID = -2484639019549527724L;
332 final String method;
333 String username = "";
334 String realm = "";
335 String nonce = "";
336 String nc = "";
337 String cnonce = "";
338 String qop = "";
339 String uri = "";
340 String response = "";
341
342
343 Digest(String m)
344 {
345 method = m;
346 }
347
348
349 @Override
350 public boolean check(Object credentials)
351 {
352 if (credentials instanceof char[])
353 credentials=new String((char[])credentials);
354 String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
355
356 try
357 {
358 MessageDigest md = MessageDigest.getInstance("MD5");
359 byte[] ha1;
360 if (credentials instanceof Credential.MD5)
361 {
362
363
364
365 ha1 = ((Credential.MD5) credentials).getDigest();
366 }
367 else
368 {
369
370 md.update(username.getBytes(StandardCharsets.ISO_8859_1));
371 md.update((byte) ':');
372 md.update(realm.getBytes(StandardCharsets.ISO_8859_1));
373 md.update((byte) ':');
374 md.update(password.getBytes(StandardCharsets.ISO_8859_1));
375 ha1 = md.digest();
376 }
377
378 md.reset();
379 md.update(method.getBytes(StandardCharsets.ISO_8859_1));
380 md.update((byte) ':');
381 md.update(uri.getBytes(StandardCharsets.ISO_8859_1));
382 byte[] ha2 = md.digest();
383
384
385
386
387
388
389
390
391 md.update(TypeUtil.toString(ha1, 16).getBytes(StandardCharsets.ISO_8859_1));
392 md.update((byte) ':');
393 md.update(nonce.getBytes(StandardCharsets.ISO_8859_1));
394 md.update((byte) ':');
395 md.update(nc.getBytes(StandardCharsets.ISO_8859_1));
396 md.update((byte) ':');
397 md.update(cnonce.getBytes(StandardCharsets.ISO_8859_1));
398 md.update((byte) ':');
399 md.update(qop.getBytes(StandardCharsets.ISO_8859_1));
400 md.update((byte) ':');
401 md.update(TypeUtil.toString(ha2, 16).getBytes(StandardCharsets.ISO_8859_1));
402 byte[] digest = md.digest();
403
404
405 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
406 }
407 catch (Exception e)
408 {
409 LOG.warn(e);
410 }
411
412 return false;
413 }
414
415 @Override
416 public String toString()
417 {
418 return username + "," + response;
419 }
420 }
421 }