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