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