001: /*
002: * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/auth/DigestScheme.java,v 1.22 2004/12/30 11:01:27 oglueck Exp $
003: * $Revision: 480424 $
004: * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
005: *
006: * ====================================================================
007: *
008: * Licensed to the Apache Software Foundation (ASF) under one or more
009: * contributor license agreements. See the NOTICE file distributed with
010: * this work for additional information regarding copyright ownership.
011: * The ASF licenses this file to You under the Apache License, Version 2.0
012: * (the "License"); you may not use this file except in compliance with
013: * the License. You may obtain a copy of the License at
014: *
015: * http://www.apache.org/licenses/LICENSE-2.0
016: *
017: * Unless required by applicable law or agreed to in writing, software
018: * distributed under the License is distributed on an "AS IS" BASIS,
019: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
020: * See the License for the specific language governing permissions and
021: * limitations under the License.
022: * ====================================================================
023: *
024: * This software consists of voluntary contributions made by many
025: * individuals on behalf of the Apache Software Foundation. For more
026: * information on the Apache Software Foundation, please see
027: * <http://www.apache.org/>.
028: *
029: */
030:
031: package org.apache.commons.httpclient.auth;
032:
033: import java.security.MessageDigest;
034: import java.security.NoSuchAlgorithmException;
035: import java.util.ArrayList;
036: import java.util.List;
037: import java.util.StringTokenizer;
038:
039: import org.apache.commons.httpclient.Credentials;
040: import org.apache.commons.httpclient.HttpClientError;
041: import org.apache.commons.httpclient.HttpMethod;
042: import org.apache.commons.httpclient.NameValuePair;
043: import org.apache.commons.httpclient.UsernamePasswordCredentials;
044: import org.apache.commons.httpclient.util.EncodingUtil;
045: import org.apache.commons.httpclient.util.ParameterFormatter;
046: import org.apache.commons.logging.Log;
047: import org.apache.commons.logging.LogFactory;
048:
049: /**
050: * <p>
051: * Digest authentication scheme as defined in RFC 2617.
052: * Both MD5 (default) and MD5-sess are supported.
053: * Currently only qop=auth or no qop is supported. qop=auth-int
054: * is unsupported. If auth and auth-int are provided, auth is
055: * used.
056: * </p>
057: * <p>
058: * Credential charset is configured via the
059: * {@link org.apache.commons.httpclient.params.HttpMethodParams#CREDENTIAL_CHARSET credential
060: * charset} parameter. Since the digest username is included as clear text in the generated
061: * Authentication header, the charset of the username must be compatible with the
062: * {@link org.apache.commons.httpclient.params.HttpMethodParams#HTTP_ELEMENT_CHARSET http element
063: * charset}.
064: * </p>
065: * TODO: make class more stateful regarding repeated authentication requests
066: *
067: * @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
068: * @author Rodney Waldhoff
069: * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
070: * @author Ortwin Gl?ck
071: * @author Sean C. Sullivan
072: * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a>
073: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
074: * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
075: */
076:
077: public class DigestScheme extends RFC2617Scheme {
078:
079: /** Log object for this class. */
080: private static final Log LOG = LogFactory
081: .getLog(DigestScheme.class);
082:
083: /**
084: * Hexa values used when creating 32 character long digest in HTTP DigestScheme
085: * in case of authentication.
086: *
087: * @see #encode(byte[])
088: */
089: private static final char[] HEXADECIMAL = { '0', '1', '2', '3',
090: '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
091:
092: /** Whether the digest authentication process is complete */
093: private boolean complete;
094:
095: //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay
096: private static final String NC = "00000001"; //nonce-count is always 1
097: private static final int QOP_MISSING = 0;
098: private static final int QOP_AUTH_INT = 1;
099: 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: /**
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(
168: "missing realm in challange");
169: }
170: if (getParameter("nonce") == null) {
171: throw new MalformedChallengeException(
172: "missing nonce in challange");
173: }
174:
175: boolean unsupportedQop = false;
176: // qop parsing
177: String qop = getParameter("qop");
178: if (qop != null) {
179: StringTokenizer tok = new StringTokenizer(qop, ",");
180: while (tok.hasMoreTokens()) {
181: String variant = tok.nextToken().trim();
182: if (variant.equals("auth")) {
183: qopVariant = QOP_AUTH;
184: break; //that's our favourite, because auth-int is unsupported
185: } else if (variant.equals("auth-int")) {
186: qopVariant = QOP_AUTH_INT;
187: } else {
188: unsupportedQop = true;
189: LOG.warn("Unsupported qop detected: " + variant);
190: }
191: }
192: }
193:
194: if (unsupportedQop && (qopVariant == QOP_MISSING)) {
195: throw new MalformedChallengeException(
196: "None of the qop methods is supported");
197: }
198:
199: cnonce = createCnonce();
200: this .complete = true;
201: }
202:
203: /**
204: * Tests if the Digest authentication process has been completed.
205: *
206: * @return <tt>true</tt> if Digest authorization has been processed,
207: * <tt>false</tt> otherwise.
208: *
209: * @since 3.0
210: */
211: public boolean isComplete() {
212: String s = getParameter("stale");
213: if ("true".equalsIgnoreCase(s)) {
214: return false;
215: } else {
216: return this .complete;
217: }
218: }
219:
220: /**
221: * Returns textual designation of the digest authentication scheme.
222: *
223: * @return <code>digest</code>
224: */
225: public String getSchemeName() {
226: return "digest";
227: }
228:
229: /**
230: * Returns <tt>false</tt>. Digest authentication scheme is request based.
231: *
232: * @return <tt>false</tt>.
233: *
234: * @since 3.0
235: */
236: public boolean isConnectionBased() {
237: return false;
238: }
239:
240: /**
241: * Produces a digest authorization string for the given set of
242: * {@link Credentials}, method name and URI.
243: *
244: * @param credentials A set of credentials to be used for athentication
245: * @param method the name of the method that requires authorization.
246: * @param uri The URI for which authorization is needed.
247: *
248: * @throws InvalidCredentialsException if authentication credentials
249: * are not valid or not applicable for this authentication scheme
250: * @throws AuthenticationException if authorization string cannot
251: * be generated due to an authentication failure
252: *
253: * @return a digest authorization string
254: *
255: * @see org.apache.commons.httpclient.HttpMethod#getName()
256: * @see org.apache.commons.httpclient.HttpMethod#getPath()
257: *
258: * @deprecated Use {@link #authenticate(Credentials, HttpMethod)}
259: */
260: public String authenticate(Credentials credentials, String method,
261: String uri) throws AuthenticationException {
262:
263: LOG
264: .trace("enter DigestScheme.authenticate(Credentials, String, String)");
265:
266: UsernamePasswordCredentials usernamepassword = null;
267: try {
268: usernamepassword = (UsernamePasswordCredentials) credentials;
269: } catch (ClassCastException e) {
270: throw new InvalidCredentialsException(
271: "Credentials cannot be used for digest authentication: "
272: + credentials.getClass().getName());
273: }
274: getParameters().put("methodname", method);
275: getParameters().put("uri", uri);
276: String digest = createDigest(usernamepassword.getUserName(),
277: usernamepassword.getPassword());
278: return "Digest "
279: + createDigestHeader(usernamepassword.getUserName(),
280: digest);
281: }
282:
283: /**
284: * Produces a digest authorization string for the given set of
285: * {@link Credentials}, method name and URI.
286: *
287: * @param credentials A set of credentials to be used for athentication
288: * @param method The method being authenticated
289: *
290: * @throws InvalidCredentialsException if authentication credentials
291: * are not valid or not applicable for this authentication scheme
292: * @throws AuthenticationException if authorization string cannot
293: * be generated due to an authentication failure
294: *
295: * @return a digest authorization string
296: *
297: * @since 3.0
298: */
299: public String authenticate(Credentials credentials,
300: HttpMethod method) throws AuthenticationException {
301:
302: LOG
303: .trace("enter DigestScheme.authenticate(Credentials, HttpMethod)");
304:
305: UsernamePasswordCredentials usernamepassword = null;
306: try {
307: usernamepassword = (UsernamePasswordCredentials) credentials;
308: } catch (ClassCastException e) {
309: throw new InvalidCredentialsException(
310: "Credentials cannot be used for digest authentication: "
311: + credentials.getClass().getName());
312: }
313: getParameters().put("methodname", method.getName());
314: StringBuffer buffer = new StringBuffer(method.getPath());
315: String query = method.getQueryString();
316: if (query != null) {
317: if (query.indexOf("?") != 0) {
318: buffer.append("?");
319: }
320: buffer.append(method.getQueryString());
321: }
322: getParameters().put("uri", buffer.toString());
323: String charset = getParameter("charset");
324: if (charset == null) {
325: getParameters().put("charset",
326: method.getParams().getCredentialCharset());
327: }
328: String digest = createDigest(usernamepassword.getUserName(),
329: usernamepassword.getPassword());
330: return "Digest "
331: + createDigestHeader(usernamepassword.getUserName(),
332: digest);
333: }
334:
335: /**
336: * Creates an MD5 response digest.
337: *
338: * @param uname Username
339: * @param pwd Password
340: * @param charset The credential charset
341: *
342: * @return The created digest as string. This will be the response tag's
343: * value in the Authentication HTTP header.
344: * @throws AuthenticationException when MD5 is an unsupported algorithm
345: */
346: private String createDigest(final String uname, final String pwd)
347: throws AuthenticationException {
348:
349: LOG
350: .trace("enter DigestScheme.createDigest(String, String, Map)");
351:
352: final String digAlg = "MD5";
353:
354: // Collecting required tokens
355: String uri = getParameter("uri");
356: String realm = getParameter("realm");
357: String nonce = getParameter("nonce");
358: String qop = getParameter("qop");
359: String method = getParameter("methodname");
360: String algorithm = getParameter("algorithm");
361: // If an algorithm is not specified, default to MD5.
362: if (algorithm == null) {
363: algorithm = "MD5";
364: }
365: // If an charset is not specified, default to ISO-8859-1.
366: String charset = getParameter("charset");
367: if (charset == null) {
368: charset = "ISO-8859-1";
369: }
370:
371: if (qopVariant == QOP_AUTH_INT) {
372: LOG.warn("qop=auth-int is not supported");
373: throw new AuthenticationException(
374: "Unsupported qop in HTTP Digest authentication");
375: }
376:
377: MessageDigest md5Helper;
378:
379: try {
380: md5Helper = MessageDigest.getInstance(digAlg);
381: } catch (Exception e) {
382: throw new AuthenticationException(
383: "Unsupported algorithm in HTTP Digest authentication: "
384: + digAlg);
385: }
386:
387: // 3.2.2.2: Calculating digest
388: StringBuffer tmp = new StringBuffer(uname.length()
389: + realm.length() + pwd.length() + 2);
390: tmp.append(uname);
391: tmp.append(':');
392: tmp.append(realm);
393: tmp.append(':');
394: tmp.append(pwd);
395: // unq(username-value) ":" unq(realm-value) ":" passwd
396: String a1 = tmp.toString();
397: //a1 is suitable for MD5 algorithm
398: if (algorithm.equals("MD5-sess")) {
399: // H( unq(username-value) ":" unq(realm-value) ":" passwd )
400: // ":" unq(nonce-value)
401: // ":" unq(cnonce-value)
402:
403: String tmp2 = encode(md5Helper.digest(EncodingUtil
404: .getBytes(a1, charset)));
405: StringBuffer tmp3 = new StringBuffer(tmp2.length()
406: + nonce.length() + cnonce.length() + 2);
407: tmp3.append(tmp2);
408: tmp3.append(':');
409: tmp3.append(nonce);
410: tmp3.append(':');
411: tmp3.append(cnonce);
412: a1 = tmp3.toString();
413: } else if (!algorithm.equals("MD5")) {
414: LOG.warn("Unhandled algorithm " + algorithm + " requested");
415: }
416: String md5a1 = encode(md5Helper.digest(EncodingUtil.getBytes(
417: a1, charset)));
418:
419: String a2 = null;
420: if (qopVariant == QOP_AUTH_INT) {
421: LOG.error("Unhandled qop auth-int");
422: //we do not have access to the entity-body or its hash
423: //TODO: add Method ":" digest-uri-value ":" H(entity-body)
424: } else {
425: a2 = method + ":" + uri;
426: }
427: String md5a2 = encode(md5Helper.digest(EncodingUtil
428: .getAsciiBytes(a2)));
429:
430: // 3.2.2.1
431: String serverDigestValue;
432: if (qopVariant == QOP_MISSING) {
433: LOG.debug("Using null qop method");
434: StringBuffer tmp2 = new StringBuffer(md5a1.length()
435: + nonce.length() + md5a2.length());
436: tmp2.append(md5a1);
437: tmp2.append(':');
438: tmp2.append(nonce);
439: tmp2.append(':');
440: tmp2.append(md5a2);
441: serverDigestValue = tmp2.toString();
442: } else {
443: if (LOG.isDebugEnabled()) {
444: LOG.debug("Using qop method " + qop);
445: }
446: String qopOption = getQopVariantString();
447: StringBuffer tmp2 = new StringBuffer(md5a1.length()
448: + nonce.length() + NC.length() + cnonce.length()
449: + qopOption.length() + md5a2.length() + 5);
450: tmp2.append(md5a1);
451: tmp2.append(':');
452: tmp2.append(nonce);
453: tmp2.append(':');
454: tmp2.append(NC);
455: tmp2.append(':');
456: tmp2.append(cnonce);
457: tmp2.append(':');
458: tmp2.append(qopOption);
459: tmp2.append(':');
460: tmp2.append(md5a2);
461: serverDigestValue = tmp2.toString();
462: }
463:
464: String serverDigest = encode(md5Helper.digest(EncodingUtil
465: .getAsciiBytes(serverDigestValue)));
466:
467: return serverDigest;
468: }
469:
470: /**
471: * Creates digest-response header as defined in RFC2617.
472: *
473: * @param uname Username
474: * @param digest The response tag's value as String.
475: *
476: * @return The digest-response as String.
477: */
478: private String createDigestHeader(final String uname,
479: final String digest) throws AuthenticationException {
480:
481: LOG.trace("enter DigestScheme.createDigestHeader(String, Map, "
482: + "String)");
483:
484: String uri = getParameter("uri");
485: String realm = getParameter("realm");
486: String nonce = getParameter("nonce");
487: String opaque = getParameter("opaque");
488: String response = digest;
489: String algorithm = getParameter("algorithm");
490:
491: List params = new ArrayList(20);
492: params.add(new NameValuePair("username", uname));
493: params.add(new NameValuePair("realm", realm));
494: params.add(new NameValuePair("nonce", nonce));
495: params.add(new NameValuePair("uri", uri));
496: params.add(new NameValuePair("response", response));
497:
498: if (qopVariant != QOP_MISSING) {
499: params.add(new NameValuePair("qop", getQopVariantString()));
500: params.add(new NameValuePair("nc", NC));
501: params.add(new NameValuePair("cnonce", this .cnonce));
502: }
503: if (algorithm != null) {
504: params.add(new NameValuePair("algorithm", algorithm));
505: }
506: if (opaque != null) {
507: params.add(new NameValuePair("opaque", opaque));
508: }
509:
510: StringBuffer buffer = new StringBuffer();
511: for (int i = 0; i < params.size(); i++) {
512: NameValuePair param = (NameValuePair) params.get(i);
513: if (i > 0) {
514: buffer.append(", ");
515: }
516: boolean noQuotes = "nc".equals(param.getName())
517: || "qop".equals(param.getName());
518: this .formatter.setAlwaysUseQuotes(!noQuotes);
519: this .formatter.format(buffer, param);
520: }
521: return buffer.toString();
522: }
523:
524: private String getQopVariantString() {
525: String qopOption;
526: if (qopVariant == QOP_AUTH_INT) {
527: qopOption = "auth-int";
528: } else {
529: qopOption = "auth";
530: }
531: return qopOption;
532: }
533:
534: /**
535: * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
536: * <CODE>String</CODE> according to RFC 2617.
537: *
538: * @param binaryData array containing the digest
539: * @return encoded MD5, or <CODE>null</CODE> if encoding failed
540: */
541: private static String encode(byte[] binaryData) {
542: LOG.trace("enter DigestScheme.encode(byte[])");
543:
544: if (binaryData.length != 16) {
545: return null;
546: }
547:
548: char[] buffer = new char[32];
549: for (int i = 0; i < 16; i++) {
550: int low = (int) (binaryData[i] & 0x0f);
551: int high = (int) ((binaryData[i] & 0xf0) >> 4);
552: buffer[i * 2] = HEXADECIMAL[high];
553: buffer[(i * 2) + 1] = HEXADECIMAL[low];
554: }
555:
556: return new String(buffer);
557: }
558:
559: /**
560: * Creates a random cnonce value based on the current time.
561: *
562: * @return The cnonce value as String.
563: * @throws HttpClientError if MD5 algorithm is not supported.
564: */
565: public static String createCnonce() {
566: LOG.trace("enter DigestScheme.createCnonce()");
567:
568: String cnonce;
569: final String digAlg = "MD5";
570: MessageDigest md5Helper;
571:
572: try {
573: md5Helper = MessageDigest.getInstance(digAlg);
574: } catch (NoSuchAlgorithmException e) {
575: throw new HttpClientError(
576: "Unsupported algorithm in HTTP Digest authentication: "
577: + digAlg);
578: }
579:
580: cnonce = Long.toString(System.currentTimeMillis());
581: cnonce = encode(md5Helper.digest(EncodingUtil
582: .getAsciiBytes(cnonce)));
583:
584: return cnonce;
585: }
586: }
|