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