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