1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 package org.apache.commons.httpclient.auth;
31
32 import java.security.MessageDigest;
33 import java.security.NoSuchAlgorithmException;
34 import java.util.StringTokenizer;
35
36 import org.apache.commons.httpclient.Credentials;
37 import org.apache.commons.httpclient.HttpClientError;
38 import org.apache.commons.httpclient.HttpMethod;
39 import org.apache.commons.httpclient.UsernamePasswordCredentials;
40 import org.apache.commons.httpclient.util.EncodingUtil;
41 import org.apache.commons.logging.Log;
42 import org.apache.commons.logging.LogFactory;
43
44 /***
45 * <p>
46 * Digest authentication scheme as defined in RFC 2617.
47 * Both MD5 (default) and MD5-sess are supported.
48 * Currently only qop=auth or no qop is supported. qop=auth-int
49 * is unsupported. If auth and auth-int are provided, auth is
50 * used.
51 * </p>
52 * <p>
53 * Credential charset is configured via the
54 * {@link org.apache.commons.httpclient.params.HttpMethodParams#CREDENTIAL_CHARSET credential
55 * charset} parameter. Since the digest username is included as clear text in the generated
56 * Authentication header, the charset of the username must be compatible with the
57 * {@link org.apache.commons.httpclient.params.HttpMethodParams#HTTP_ELEMENT_CHARSET http element
58 * charset}.
59 * </p>
60 * TODO: make class more stateful regarding repeated authentication requests
61 *
62 * @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
63 * @author Rodney Waldhoff
64 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
65 * @author Ortwin Gl?ck
66 * @author Sean C. Sullivan
67 * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a>
68 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
69 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
70 */
71
72 public class DigestScheme extends RFC2617Scheme {
73
74 /*** Log object for this class. */
75 private static final Log LOG = LogFactory.getLog(DigestScheme.class);
76
77 /***
78 * Hexa values used when creating 32 character long digest in HTTP DigestScheme
79 * in case of authentication.
80 *
81 * @see #encode(byte[])
82 */
83 private static final char[] HEXADECIMAL = {
84 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
85 'e', 'f'
86 };
87
88 /*** Whether the digest authentication process is complete */
89 private boolean complete;
90
91
92 private static final String NC = "00000001";
93 private static final int QOP_MISSING = 0;
94 private static final int QOP_AUTH_INT = 1;
95 private static final int QOP_AUTH = 2;
96
97 private int qopVariant = QOP_MISSING;
98 private String cnonce;
99
100 /***
101 * Default constructor for the digest authetication scheme.
102 *
103 * @since 3.0
104 */
105 public DigestScheme() {
106 super();
107 this.complete = false;
108 }
109
110 /***
111 * Gets an ID based upon the realm and the nonce value. This ensures that requests
112 * to the same realm with different nonce values will succeed. This differentiation
113 * allows servers to request re-authentication using a fresh nonce value.
114 *
115 * @deprecated no longer used
116 */
117 public String getID() {
118
119 String id = getRealm();
120 String nonce = getParameter("nonce");
121 if (nonce != null) {
122 id += "-" + nonce;
123 }
124
125 return id;
126 }
127
128 /***
129 * Constructor for the digest authetication scheme.
130 *
131 * @param challenge authentication challenge
132 *
133 * @throws MalformedChallengeException is thrown if the authentication challenge
134 * is malformed
135 *
136 * @deprecated Use parameterless constructor and {@link AuthScheme#processChallenge(String)}
137 * method
138 */
139 public DigestScheme(final String challenge)
140 throws MalformedChallengeException {
141 super(challenge);
142 this.complete = true;
143 }
144
145 /***
146 * Processes the Digest challenge.
147 *
148 * @param challenge the challenge string
149 *
150 * @throws MalformedChallengeException is thrown if the authentication challenge
151 * is malformed
152 *
153 * @since 3.0
154 */
155 public void processChallenge(final String challenge)
156 throws MalformedChallengeException {
157 super.processChallenge(challenge);
158
159 if (getParameter("realm") == null) {
160 throw new MalformedChallengeException("missing realm in challange");
161 }
162 if (getParameter("nonce") == null) {
163 throw new MalformedChallengeException("missing nonce in challange");
164 }
165
166 boolean unsupportedQop = false;
167
168 String qop = getParameter("qop");
169 if (qop != null) {
170 StringTokenizer tok = new StringTokenizer(qop,",");
171 while (tok.hasMoreTokens()) {
172 String variant = tok.nextToken().trim();
173 if (variant.equals("auth")) {
174 qopVariant = QOP_AUTH;
175 break;
176 } else if (variant.equals("auth-int")) {
177 qopVariant = QOP_AUTH_INT;
178 } else {
179 unsupportedQop = true;
180 LOG.warn("Unsupported qop detected: "+ variant);
181 }
182 }
183 }
184
185 if (unsupportedQop && (qopVariant == QOP_MISSING)) {
186 throw new MalformedChallengeException("None of the qop methods is supported");
187 }
188
189 cnonce = createCnonce();
190 this.complete = true;
191 }
192
193 /***
194 * Tests if the Digest authentication process has been completed.
195 *
196 * @return <tt>true</tt> if Digest authorization has been processed,
197 * <tt>false</tt> otherwise.
198 *
199 * @since 3.0
200 */
201 public boolean isComplete() {
202 String s = getParameter("stale");
203 if ("true".equalsIgnoreCase(s)) {
204 return false;
205 } else {
206 return this.complete;
207 }
208 }
209
210 /***
211 * Returns textual designation of the digest authentication scheme.
212 *
213 * @return <code>digest</code>
214 */
215 public String getSchemeName() {
216 return "digest";
217 }
218
219 /***
220 * Returns <tt>false</tt>. Digest authentication scheme is request based.
221 *
222 * @return <tt>false</tt>.
223 *
224 * @since 3.0
225 */
226 public boolean isConnectionBased() {
227 return false;
228 }
229
230 /***
231 * Produces a digest authorization string for the given set of
232 * {@link Credentials}, method name and URI.
233 *
234 * @param credentials A set of credentials to be used for athentication
235 * @param method the name of the method that requires authorization.
236 * @param uri The URI for which authorization is needed.
237 *
238 * @throws InvalidCredentialsException if authentication credentials
239 * are not valid or not applicable for this authentication scheme
240 * @throws AuthenticationException if authorization string cannot
241 * be generated due to an authentication failure
242 *
243 * @return a digest authorization string
244 *
245 * @see org.apache.commons.httpclient.HttpMethod#getName()
246 * @see org.apache.commons.httpclient.HttpMethod#getPath()
247 *
248 * @deprecated Use {@link #authenticate(Credentials, HttpMethod)}
249 */
250 public String authenticate(Credentials credentials, String method, String uri)
251 throws AuthenticationException {
252
253 LOG.trace("enter DigestScheme.authenticate(Credentials, String, String)");
254
255 UsernamePasswordCredentials usernamepassword = null;
256 try {
257 usernamepassword = (UsernamePasswordCredentials) credentials;
258 } catch (ClassCastException e) {
259 throw new InvalidCredentialsException(
260 "Credentials cannot be used for digest authentication: "
261 + credentials.getClass().getName());
262 }
263 this.getParameters().put("methodname", method);
264 this.getParameters().put("uri", uri);
265 String digest = createDigest(
266 usernamepassword.getUserName(),
267 usernamepassword.getPassword(),
268 "ISO-8859-1");
269
270 return "Digest " + createDigestHeader(usernamepassword.getUserName(), digest);
271 }
272
273 /***
274 * Produces a digest authorization string for the given set of
275 * {@link Credentials}, method name and URI.
276 *
277 * @param credentials A set of credentials to be used for athentication
278 * @param method The method being authenticated
279 *
280 * @throws InvalidCredentialsException if authentication credentials
281 * are not valid or not applicable for this authentication scheme
282 * @throws AuthenticationException if authorization string cannot
283 * be generated due to an authentication failure
284 *
285 * @return a digest authorization string
286 *
287 * @since 3.0
288 */
289 public String authenticate(Credentials credentials, HttpMethod method)
290 throws AuthenticationException {
291
292 LOG.trace("enter DigestScheme.authenticate(Credentials, HttpMethod)");
293
294 UsernamePasswordCredentials usernamepassword = null;
295 try {
296 usernamepassword = (UsernamePasswordCredentials) credentials;
297 } catch (ClassCastException e) {
298 throw new InvalidCredentialsException(
299 "Credentials cannot be used for digest authentication: "
300 + credentials.getClass().getName());
301 }
302 this.getParameters().put("methodname", method.getName());
303 this.getParameters().put("uri", method.getPath());
304 String digest = createDigest(
305 usernamepassword.getUserName(),
306 usernamepassword.getPassword(),
307 method.getParams().getCredentialCharset());
308
309 return "Digest " + createDigestHeader(usernamepassword.getUserName(),
310 digest);
311 }
312
313 /***
314 * Creates an MD5 response digest.
315 *
316 * @param uname Username
317 * @param pwd Password
318 * @param charset The credential charset
319 *
320 * @return The created digest as string. This will be the response tag's
321 * value in the Authentication HTTP header.
322 * @throws AuthenticationException when MD5 is an unsupported algorithm
323 */
324 private String createDigest(String uname, String pwd, String charset) throws AuthenticationException {
325
326 LOG.trace("enter DigestScheme.createDigest(String, String, Map)");
327
328 final String digAlg = "MD5";
329
330
331 String uri = getParameter("uri");
332 String realm = getParameter("realm");
333 String nonce = getParameter("nonce");
334 String qop = getParameter("qop");
335 String method = getParameter("methodname");
336 String algorithm = getParameter("algorithm");
337
338
339 if(algorithm == null) {
340 algorithm="MD5";
341 }
342
343 if (qopVariant == QOP_AUTH_INT) {
344 LOG.warn("qop=auth-int is not supported");
345 throw new AuthenticationException(
346 "Unsupported qop in HTTP Digest authentication");
347 }
348
349 MessageDigest md5Helper;
350
351 try {
352 md5Helper = MessageDigest.getInstance(digAlg);
353 } catch (Exception e) {
354 throw new AuthenticationException(
355 "Unsupported algorithm in HTTP Digest authentication: "
356 + digAlg);
357 }
358
359
360 StringBuffer tmp = new StringBuffer(uname.length() + realm.length() + pwd.length() + 2);
361 tmp.append(uname);
362 tmp.append(':');
363 tmp.append(realm);
364 tmp.append(':');
365 tmp.append(pwd);
366
367 String a1 = tmp.toString();
368
369 if(algorithm.equals("MD5-sess")) {
370
371
372
373
374 String tmp2=encode(md5Helper.digest(EncodingUtil.getBytes(a1, charset)));
375 StringBuffer tmp3 = new StringBuffer(tmp2.length() + nonce.length() + cnonce.length() + 2);
376 tmp3.append(tmp2);
377 tmp3.append(':');
378 tmp3.append(nonce);
379 tmp3.append(':');
380 tmp3.append(cnonce);
381 a1 = tmp3.toString();
382 } else if(!algorithm.equals("MD5")) {
383 LOG.warn("Unhandled algorithm " + algorithm + " requested");
384 }
385 String md5a1 = encode(md5Helper.digest(EncodingUtil.getBytes(a1, charset)));
386
387 String a2 = null;
388 if (qopVariant == QOP_AUTH_INT) {
389 LOG.error("Unhandled qop auth-int");
390
391
392 } else {
393 a2 = method + ":" + uri;
394 }
395 String md5a2 = encode(md5Helper.digest(EncodingUtil.getAsciiBytes(a2)));
396
397
398 String serverDigestValue;
399 if (qopVariant == QOP_MISSING) {
400 LOG.debug("Using null qop method");
401 StringBuffer tmp2 = new StringBuffer(md5a1.length() + nonce.length() + md5a2.length());
402 tmp2.append(md5a1);
403 tmp2.append(':');
404 tmp2.append(nonce);
405 tmp2.append(':');
406 tmp2.append(md5a2);
407 serverDigestValue = tmp2.toString();
408 } else {
409 if (LOG.isDebugEnabled()) {
410 LOG.debug("Using qop method " + qop);
411 }
412 String qopOption = getQopVariantString();
413 StringBuffer tmp2 = new StringBuffer(md5a1.length() + nonce.length()
414 + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5);
415 tmp2.append(md5a1);
416 tmp2.append(':');
417 tmp2.append(nonce);
418 tmp2.append(':');
419 tmp2.append(NC);
420 tmp2.append(':');
421 tmp2.append(cnonce);
422 tmp2.append(':');
423 tmp2.append(qopOption);
424 tmp2.append(':');
425 tmp2.append(md5a2);
426 serverDigestValue = tmp2.toString();
427 }
428
429 String serverDigest =
430 encode(md5Helper.digest(EncodingUtil.getAsciiBytes(serverDigestValue)));
431
432 return serverDigest;
433 }
434
435 /***
436 * Creates digest-response header as defined in RFC2617.
437 *
438 * @param uname Username
439 * @param digest The response tag's value as String.
440 *
441 * @return The digest-response as String.
442 */
443 private String createDigestHeader(String uname, String digest) throws AuthenticationException {
444
445 LOG.trace("enter DigestScheme.createDigestHeader(String, Map, "
446 + "String)");
447
448 StringBuffer sb = new StringBuffer();
449 String uri = getParameter("uri");
450 String realm = getParameter("realm");
451 String nonce = getParameter("nonce");
452 String nc = getParameter("nc");
453 String opaque = getParameter("opaque");
454 String response = digest;
455 String qop = getParameter("qop");
456 String algorithm = getParameter("algorithm");
457
458 sb.append("username=\"" + uname + "\"")
459 .append(", realm=\"" + realm + "\"")
460 .append(", nonce=\"" + nonce + "\"").append(", uri=\"" + uri + "\"")
461 .append(", response=\"" + response + "\"");
462 if (qopVariant != QOP_MISSING) {
463 sb.append(", qop=\"" + getQopVariantString() + "\"")
464 .append(", nc="+ NC)
465 .append(", cnonce=\"" + cnonce + "\"");
466 }
467 if (algorithm != null) {
468 sb.append(", algorithm=\"" + algorithm + "\"");
469 }
470 if (opaque != null) {
471 sb.append(", opaque=\"" + opaque + "\"");
472 }
473 return sb.toString();
474 }
475
476 private String getQopVariantString() {
477 String qopOption;
478 if (qopVariant == QOP_AUTH_INT) {
479 qopOption = "auth-int";
480 } else {
481 qopOption = "auth";
482 }
483 return qopOption;
484 }
485
486 /***
487 * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
488 * <CODE>String</CODE> according to RFC 2617.
489 *
490 * @param binaryData array containing the digest
491 * @return encoded MD5, or <CODE>null</CODE> if encoding failed
492 */
493 private static String encode(byte[] binaryData) {
494 LOG.trace("enter DigestScheme.encode(byte[])");
495
496 if (binaryData.length != 16) {
497 return null;
498 }
499
500 char[] buffer = new char[32];
501 for (int i = 0; i < 16; i++) {
502 int low = (int) (binaryData[i] & 0x0f);
503 int high = (int) ((binaryData[i] & 0xf0) >> 4);
504 buffer[i * 2] = HEXADECIMAL[high];
505 buffer[(i * 2) + 1] = HEXADECIMAL[low];
506 }
507
508 return new String(buffer);
509 }
510
511
512 /***
513 * Creates a random cnonce value based on the current time.
514 *
515 * @return The cnonce value as String.
516 * @throws HttpClientError if MD5 algorithm is not supported.
517 */
518 public static String createCnonce() {
519 LOG.trace("enter DigestScheme.createCnonce()");
520
521 String cnonce;
522 final String digAlg = "MD5";
523 MessageDigest md5Helper;
524
525 try {
526 md5Helper = MessageDigest.getInstance(digAlg);
527 } catch (NoSuchAlgorithmException e) {
528 throw new HttpClientError(
529 "Unsupported algorithm in HTTP Digest authentication: "
530 + digAlg);
531 }
532
533 cnonce = Long.toString(System.currentTimeMillis());
534 cnonce = encode(md5Helper.digest(EncodingUtil.getAsciiBytes(cnonce)));
535
536 return cnonce;
537 }
538 }