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