View Javadoc

1   /*
2    * $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 $
3    * $Revision: 155418 $
4    * $Date: 2005-02-26 08:01:52 -0500 (Sat, 26 Feb 2005) $
5    *
6    * ====================================================================
7    *
8    *  Copyright 2002-2004 The Apache Software Foundation
9    *
10   *  Licensed under the Apache License, Version 2.0 (the "License");
11   *  you may not use this file except in compliance with the License.
12   *  You may obtain a copy of the License at
13   *
14   *      http://www.apache.org/licenses/LICENSE-2.0
15   *
16   *  Unless required by applicable law or agreed to in writing, software
17   *  distributed under the License is distributed on an "AS IS" BASIS,
18   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   *  See the License for the specific language governing permissions and
20   *  limitations under the License.
21   * ====================================================================
22   *
23   * This software consists of voluntary contributions made by many
24   * individuals on behalf of the Apache Software Foundation.  For more
25   * information on the Apache Software Foundation, please see
26   * <http://www.apache.org/>.
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      //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay  
92      private static final String NC = "00000001"; //nonce-count is always 1
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         // qop parsing
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; //that's our favourite, because auth-int is unsupported
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         // Collecting required tokens
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         // If an algorithm is not specified, default to MD5.
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         // 3.2.2.2: Calculating digest
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         // unq(username-value) ":" unq(realm-value) ":" passwd
367         String a1 = tmp.toString();
368         //a1 is suitable for MD5 algorithm
369         if(algorithm.equals("MD5-sess")) {
370             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
371             //      ":" unq(nonce-value)
372             //      ":" unq(cnonce-value)
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             //we do not have access to the entity-body or its hash
391             //TODO: add Method ":" digest-uri-value ":" H(entity-body)      
392         } else {
393             a2 = method + ":" + uri;
394         }
395         String md5a2 = encode(md5Helper.digest(EncodingUtil.getAsciiBytes(a2)));
396 
397         // 3.2.2.1
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 }