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