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