1
2
3
4
5
6
7
8
9
10
11
12
13
14 package org.eclipse.jetty.security.jaspi.modules;
15
16 import java.io.IOException;
17 import java.security.MessageDigest;
18 import java.util.Map;
19
20 import javax.security.auth.Subject;
21 import javax.security.auth.callback.CallbackHandler;
22 import javax.security.auth.callback.UnsupportedCallbackException;
23 import javax.security.auth.message.AuthException;
24 import javax.security.auth.message.AuthStatus;
25 import javax.security.auth.message.MessageInfo;
26 import javax.security.auth.message.MessagePolicy;
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.http.security.Constraint;
32 import org.eclipse.jetty.http.security.Credential;
33 import org.eclipse.jetty.util.B64Code;
34 import org.eclipse.jetty.util.QuotedStringTokenizer;
35 import org.eclipse.jetty.util.StringUtil;
36 import org.eclipse.jetty.util.TypeUtil;
37 import org.eclipse.jetty.util.log.Log;
38 import org.eclipse.jetty.util.log.Logger;
39
40
41
42
43
44 public class DigestAuthModule extends BaseAuthModule
45 {
46 private static final Logger LOG = Log.getLogger(DigestAuthModule.class);
47
48
49 protected long maxNonceAge = 0;
50
51 protected long nonceSecret = this.hashCode() ^ System.currentTimeMillis();
52
53 protected boolean useStale = false;
54
55 private String realmName;
56
57 private static final String REALM_KEY = "org.eclipse.jetty.security.jaspi.modules.RealmName";
58
59 public DigestAuthModule()
60 {
61 }
62
63 public DigestAuthModule(CallbackHandler callbackHandler, String realmName)
64 {
65 super(callbackHandler);
66 this.realmName = realmName;
67 }
68
69 @Override
70 public void initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy,
71 CallbackHandler handler, Map options)
72 throws AuthException
73 {
74 super.initialize(requestPolicy, responsePolicy, handler, options);
75 realmName = (String) options.get(REALM_KEY);
76 }
77
78 @Override
79 public AuthStatus validateRequest(MessageInfo messageInfo, Subject clientSubject,
80 Subject serviceSubject)
81 throws AuthException
82 {
83 HttpServletRequest request = (HttpServletRequest) messageInfo.getRequestMessage();
84 HttpServletResponse response = (HttpServletResponse) messageInfo.getResponseMessage();
85 String credentials = request.getHeader(HttpHeaders.AUTHORIZATION);
86
87 try
88 {
89 boolean stale = false;
90
91 long timestamp = System.currentTimeMillis();
92 if (credentials != null)
93 {
94 if (LOG.isDebugEnabled()) LOG.debug("Credentials: " + credentials);
95 QuotedStringTokenizer tokenizer = new QuotedStringTokenizer(credentials, "=, ", true, false);
96 final Digest digest = new Digest(request.getMethod());
97 String last = null;
98 String name = null;
99
100 while (tokenizer.hasMoreTokens())
101 {
102 String tok = tokenizer.nextToken();
103 char c = (tok.length() == 1) ? tok.charAt(0) : '\0';
104
105 switch (c)
106 {
107 case '=':
108 name = last;
109 last = tok;
110 break;
111 case ',':
112 name = null;
113 case ' ':
114 break;
115
116 default:
117 last = tok;
118 if (name != null)
119 {
120 if ("username".equalsIgnoreCase(name))
121 digest.username = tok;
122 else if ("realm".equalsIgnoreCase(name))
123 digest.realm = tok;
124 else if ("nonce".equalsIgnoreCase(name))
125 digest.nonce = tok;
126 else if ("nc".equalsIgnoreCase(name))
127 digest.nc = tok;
128 else if ("cnonce".equalsIgnoreCase(name))
129 digest.cnonce = tok;
130 else if ("qop".equalsIgnoreCase(name))
131 digest.qop = tok;
132 else if ("uri".equalsIgnoreCase(name))
133 digest.uri = tok;
134 else if ("response".equalsIgnoreCase(name)) digest.response = tok;
135 break;
136 }
137 }
138 }
139
140 int n = checkNonce(digest.nonce, timestamp);
141
142 if (n > 0)
143 {
144 if (login(clientSubject, digest.username, digest, Constraint.__DIGEST_AUTH, messageInfo)) { return AuthStatus.SUCCESS; }
145 }
146 else if (n == 0) stale = true;
147
148 }
149
150 if (!isMandatory(messageInfo)) { return AuthStatus.SUCCESS; }
151 String domain = request.getContextPath();
152 if (domain == null) domain = "/";
153 response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Digest realm=\"" + realmName
154 + "\", domain=\""
155 + domain
156 + "\", nonce=\""
157 + newNonce(timestamp)
158 + "\", algorithm=MD5, qop=\"auth\""
159 + (useStale ? (" stale=" + stale) : ""));
160 response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
161 return AuthStatus.SEND_CONTINUE;
162 }
163 catch (IOException e)
164 {
165 throw new AuthException(e.getMessage());
166 }
167 catch (UnsupportedCallbackException e)
168 {
169 throw new AuthException(e.getMessage());
170 }
171
172 }
173
174 public String newNonce(long ts)
175 {
176
177 long sk = nonceSecret;
178
179 byte[] nounce = new byte[24];
180 for (int i = 0; i < 8; i++)
181 {
182 nounce[i] = (byte) (ts & 0xff);
183 ts = ts >> 8;
184 nounce[8 + i] = (byte) (sk & 0xff);
185 sk = sk >> 8;
186 }
187
188 byte[] hash = null;
189 try
190 {
191 MessageDigest md = MessageDigest.getInstance("MD5");
192 md.reset();
193 md.update(nounce, 0, 16);
194 hash = md.digest();
195 }
196 catch (Exception e)
197 {
198 LOG.warn(e);
199 }
200
201 for (int i = 0; i < hash.length; i++)
202 {
203 nounce[8 + i] = hash[i];
204 if (i == 23) break;
205 }
206
207 return new String(B64Code.encode(nounce));
208 }
209
210
211
212
213
214
215
216 public int checkNonce(String nonce, long timestamp)
217 {
218 try
219 {
220 byte[] n = B64Code.decode(nonce.toCharArray());
221 if (n.length != 24) return -1;
222
223 long ts = 0;
224 long sk = nonceSecret;
225 byte[] n2 = new byte[16];
226 System.arraycopy(n, 0, n2, 0, 8);
227 for (int i = 0; i < 8; i++)
228 {
229 n2[8 + i] = (byte) (sk & 0xff);
230 sk = sk >> 8;
231 ts = (ts << 8) + (0xff & (long) n[7 - i]);
232 }
233
234 long age = timestamp - ts;
235 if (LOG.isDebugEnabled()) LOG.debug("age=" + age);
236
237 byte[] hash = null;
238 try
239 {
240 MessageDigest md = MessageDigest.getInstance("MD5");
241 md.reset();
242 md.update(n2, 0, 16);
243 hash = md.digest();
244 }
245 catch (Exception e)
246 {
247 LOG.warn(e);
248 }
249
250 for (int i = 0; i < 16; i++)
251 if (n[i + 8] != hash[i]) return -1;
252
253 if (maxNonceAge > 0 && (age < 0 || age > maxNonceAge)) return 0;
254
255 return 1;
256 }
257 catch (Exception e)
258 {
259 LOG.ignore(e);
260 }
261 return -1;
262 }
263
264 private static class Digest extends Credential
265 {
266 private static final long serialVersionUID = -1866670896275159116L;
267
268 String method = null;
269 String username = null;
270 String realm = null;
271 String nonce = null;
272 String nc = null;
273 String cnonce = null;
274 String qop = null;
275 String uri = null;
276 String response = null;
277
278
279 Digest(String m)
280 {
281 method = m;
282 }
283
284
285 @Override
286 public boolean check(Object credentials)
287 {
288 String password = (credentials instanceof String) ? (String) credentials : credentials.toString();
289
290 try
291 {
292 MessageDigest md = MessageDigest.getInstance("MD5");
293 byte[] ha1;
294 if (credentials instanceof Credential.MD5)
295 {
296
297
298
299 ha1 = ((Credential.MD5) credentials).getDigest();
300 }
301 else
302 {
303
304 md.update(username.getBytes(StringUtil.__ISO_8859_1));
305 md.update((byte) ':');
306 md.update(realm.getBytes(StringUtil.__ISO_8859_1));
307 md.update((byte) ':');
308 md.update(password.getBytes(StringUtil.__ISO_8859_1));
309 ha1 = md.digest();
310 }
311
312 md.reset();
313 md.update(method.getBytes(StringUtil.__ISO_8859_1));
314 md.update((byte) ':');
315 md.update(uri.getBytes(StringUtil.__ISO_8859_1));
316 byte[] ha2 = md.digest();
317
318
319
320
321
322
323
324
325 md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1));
326 md.update((byte) ':');
327 md.update(nonce.getBytes(StringUtil.__ISO_8859_1));
328 md.update((byte) ':');
329 md.update(nc.getBytes(StringUtil.__ISO_8859_1));
330 md.update((byte) ':');
331 md.update(cnonce.getBytes(StringUtil.__ISO_8859_1));
332 md.update((byte) ':');
333 md.update(qop.getBytes(StringUtil.__ISO_8859_1));
334 md.update((byte) ':');
335 md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1));
336 byte[] digest = md.digest();
337
338
339 return (TypeUtil.toString(digest, 16).equalsIgnoreCase(response));
340 }
341 catch (Exception e)
342 {
343 LOG.warn(e);
344 }
345
346 return false;
347 }
348
349 @Override
350 public String toString()
351 {
352 return username + "," + response;
353 }
354
355 }
356 }