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