Home | History | Annotate | Download | only in auth
      1 /*
      2  * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java $
      3  * $Revision: 659595 $
      4  * $Date: 2008-05-23 09:47:14 -0700 (Fri, 23 May 2008) $
      5  *
      6  * ====================================================================
      7  *
      8  *  Licensed to the Apache Software Foundation (ASF) under one or more
      9  *  contributor license agreements.  See the NOTICE file distributed with
     10  *  this work for additional information regarding copyright ownership.
     11  *  The ASF licenses this file to You under the Apache License, Version 2.0
     12  *  (the "License"); you may not use this file except in compliance with
     13  *  the License.  You may obtain a copy of the License at
     14  *
     15  *      http://www.apache.org/licenses/LICENSE-2.0
     16  *
     17  *  Unless required by applicable law or agreed to in writing, software
     18  *  distributed under the License is distributed on an "AS IS" BASIS,
     19  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     20  *  See the License for the specific language governing permissions and
     21  *  limitations under the License.
     22  * ====================================================================
     23  *
     24  * This software consists of voluntary contributions made by many
     25  * individuals on behalf of the Apache Software Foundation.  For more
     26  * information on the Apache Software Foundation, please see
     27  * <http://www.apache.org/>.
     28  *
     29  */
     30 
     31 package org.apache.http.impl.auth;
     32 
     33 import java.security.MessageDigest;
     34 import java.util.ArrayList;
     35 import java.util.List;
     36 import java.util.StringTokenizer;
     37 
     38 import org.apache.http.Header;
     39 import org.apache.http.HttpRequest;
     40 import org.apache.http.auth.AuthenticationException;
     41 import org.apache.http.auth.Credentials;
     42 import org.apache.http.auth.AUTH;
     43 import org.apache.http.auth.MalformedChallengeException;
     44 import org.apache.http.auth.params.AuthParams;
     45 import org.apache.http.message.BasicNameValuePair;
     46 import org.apache.http.message.BasicHeaderValueFormatter;
     47 import org.apache.http.message.BufferedHeader;
     48 import org.apache.http.util.CharArrayBuffer;
     49 import org.apache.http.util.EncodingUtils;
     50 
     51 /**
     52  * <p>
     53  * Digest authentication scheme as defined in RFC 2617.
     54  * Both MD5 (default) and MD5-sess are supported.
     55  * Currently only qop=auth or no qop is supported. qop=auth-int
     56  * is unsupported. If auth and auth-int are provided, auth is
     57  * used.
     58  * </p>
     59  * <p>
     60  * Credential charset is configured via the
     61  * {@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET
     62  *        credential charset} parameter.
     63  * Since the digest username is included as clear text in the generated
     64  * Authentication header, the charset of the username must be compatible
     65  * with the
     66  * {@link org.apache.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET
     67  *        http element charset}.
     68  * </p>
     69  *
     70  * @author <a href="mailto:remm (at) apache.org">Remy Maucherat</a>
     71  * @author Rodney Waldhoff
     72  * @author <a href="mailto:jsdever (at) apache.org">Jeff Dever</a>
     73  * @author Ortwin Glueck
     74  * @author Sean C. Sullivan
     75  * @author <a href="mailto:adrian (at) ephox.com">Adrian Sutton</a>
     76  * @author <a href="mailto:mbowler (at) GargoyleSoftware.com">Mike Bowler</a>
     77  * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
     78  *
     79  * @since 4.0
     80  */
     81 
     82 public class DigestScheme extends RFC2617Scheme {
     83 
     84     /**
     85      * Hexa values used when creating 32 character long digest in HTTP DigestScheme
     86      * in case of authentication.
     87      *
     88      * @see #encode(byte[])
     89      */
     90     private static final char[] HEXADECIMAL = {
     91         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
     92         'e', 'f'
     93     };
     94 
     95     /** Whether the digest authentication process is complete */
     96     private boolean complete;
     97 
     98     //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay
     99     private static final String NC = "00000001"; //nonce-count is always 1
    100     private static final int QOP_MISSING = 0;
    101     private static final int QOP_AUTH_INT = 1;
    102     private static final int QOP_AUTH = 2;
    103 
    104     private int qopVariant = QOP_MISSING;
    105     private String cnonce;
    106 
    107     /**
    108      * Default constructor for the digest authetication scheme.
    109      */
    110     public DigestScheme() {
    111         super();
    112         this.complete = false;
    113     }
    114 
    115     /**
    116      * Processes the Digest challenge.
    117      *
    118      * @param header the challenge header
    119      *
    120      * @throws MalformedChallengeException is thrown if the authentication challenge
    121      * is malformed
    122      */
    123     @Override
    124     public void processChallenge(
    125             final Header header) throws MalformedChallengeException {
    126         super.processChallenge(header);
    127 
    128         if (getParameter("realm") == null) {
    129             throw new MalformedChallengeException("missing realm in challange");
    130         }
    131         if (getParameter("nonce") == null) {
    132             throw new MalformedChallengeException("missing nonce in challange");
    133         }
    134 
    135         boolean unsupportedQop = false;
    136         // qop parsing
    137         String qop = getParameter("qop");
    138         if (qop != null) {
    139             StringTokenizer tok = new StringTokenizer(qop,",");
    140             while (tok.hasMoreTokens()) {
    141                 String variant = tok.nextToken().trim();
    142                 if (variant.equals("auth")) {
    143                     qopVariant = QOP_AUTH;
    144                     break; //that's our favourite, because auth-int is unsupported
    145                 } else if (variant.equals("auth-int")) {
    146                     qopVariant = QOP_AUTH_INT;
    147                 } else {
    148                     unsupportedQop = true;
    149                 }
    150             }
    151         }
    152 
    153         if (unsupportedQop && (qopVariant == QOP_MISSING)) {
    154             throw new MalformedChallengeException("None of the qop methods is supported");
    155         }
    156         // Reset cnonce
    157         this.cnonce = null;
    158         this.complete = true;
    159     }
    160 
    161     /**
    162      * Tests if the Digest authentication process has been completed.
    163      *
    164      * @return <tt>true</tt> if Digest authorization has been processed,
    165      *   <tt>false</tt> otherwise.
    166      */
    167     public boolean isComplete() {
    168         String s = getParameter("stale");
    169         if ("true".equalsIgnoreCase(s)) {
    170             return false;
    171         } else {
    172             return this.complete;
    173         }
    174     }
    175 
    176     /**
    177      * Returns textual designation of the digest authentication scheme.
    178      *
    179      * @return <code>digest</code>
    180      */
    181     public String getSchemeName() {
    182         return "digest";
    183     }
    184 
    185     /**
    186      * Returns <tt>false</tt>. Digest authentication scheme is request based.
    187      *
    188      * @return <tt>false</tt>.
    189      */
    190     public boolean isConnectionBased() {
    191         return false;
    192     }
    193 
    194     public void overrideParamter(final String name, final String value) {
    195         getParameters().put(name, value);
    196     }
    197 
    198     private String getCnonce() {
    199         if (this.cnonce == null) {
    200             this.cnonce = createCnonce();
    201         }
    202         return this.cnonce;
    203     }
    204 
    205     /**
    206      * Produces a digest authorization string for the given set of
    207      * {@link Credentials}, method name and URI.
    208      *
    209      * @param credentials A set of credentials to be used for athentication
    210      * @param request    The request being authenticated
    211      *
    212      * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
    213      *         are not valid or not applicable for this authentication scheme
    214      * @throws AuthenticationException if authorization string cannot
    215      *   be generated due to an authentication failure
    216      *
    217      * @return a digest authorization string
    218      */
    219     public Header authenticate(
    220             final Credentials credentials,
    221             final HttpRequest request) throws AuthenticationException {
    222 
    223         if (credentials == null) {
    224             throw new IllegalArgumentException("Credentials may not be null");
    225         }
    226         if (request == null) {
    227             throw new IllegalArgumentException("HTTP request may not be null");
    228         }
    229 
    230         // Add method name and request-URI to the parameter map
    231         getParameters().put("methodname", request.getRequestLine().getMethod());
    232         getParameters().put("uri", request.getRequestLine().getUri());
    233         String charset = getParameter("charset");
    234         if (charset == null) {
    235             charset = AuthParams.getCredentialCharset(request.getParams());
    236             getParameters().put("charset", charset);
    237         }
    238         String digest = createDigest(credentials);
    239         return createDigestHeader(credentials, digest);
    240     }
    241 
    242     private static MessageDigest createMessageDigest(
    243             final String digAlg) throws UnsupportedDigestAlgorithmException {
    244         try {
    245             return MessageDigest.getInstance(digAlg);
    246         } catch (Exception e) {
    247             throw new UnsupportedDigestAlgorithmException(
    248               "Unsupported algorithm in HTTP Digest authentication: "
    249                + digAlg);
    250         }
    251     }
    252 
    253     /**
    254      * Creates an MD5 response digest.
    255      *
    256      * @return The created digest as string. This will be the response tag's
    257      *         value in the Authentication HTTP header.
    258      * @throws AuthenticationException when MD5 is an unsupported algorithm
    259      */
    260     private String createDigest(final Credentials credentials) throws AuthenticationException {
    261         // Collecting required tokens
    262         String uri = getParameter("uri");
    263         String realm = getParameter("realm");
    264         String nonce = getParameter("nonce");
    265         String method = getParameter("methodname");
    266         String algorithm = getParameter("algorithm");
    267         if (uri == null) {
    268             throw new IllegalStateException("URI may not be null");
    269         }
    270         if (realm == null) {
    271             throw new IllegalStateException("Realm may not be null");
    272         }
    273         if (nonce == null) {
    274             throw new IllegalStateException("Nonce may not be null");
    275         }
    276         // If an algorithm is not specified, default to MD5.
    277         if (algorithm == null) {
    278             algorithm = "MD5";
    279         }
    280         // If an charset is not specified, default to ISO-8859-1.
    281         String charset = getParameter("charset");
    282         if (charset == null) {
    283             charset = "ISO-8859-1";
    284         }
    285 
    286         if (qopVariant == QOP_AUTH_INT) {
    287             throw new AuthenticationException(
    288                 "Unsupported qop in HTTP Digest authentication");
    289         }
    290 
    291         MessageDigest md5Helper = createMessageDigest("MD5");
    292 
    293         String uname = credentials.getUserPrincipal().getName();
    294         String pwd = credentials.getPassword();
    295 
    296         // 3.2.2.2: Calculating digest
    297         StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2);
    298         tmp.append(uname);
    299         tmp.append(':');
    300         tmp.append(realm);
    301         tmp.append(':');
    302         tmp.append(pwd);
    303         // unq(username-value) ":" unq(realm-value) ":" passwd
    304         String a1 = tmp.toString();
    305 
    306         //a1 is suitable for MD5 algorithm
    307         if(algorithm.equalsIgnoreCase("MD5-sess")) { // android-changed: ignore case
    308             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
    309             //      ":" unq(nonce-value)
    310             //      ":" unq(cnonce-value)
    311 
    312             String cnonce = getCnonce();
    313 
    314             String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
    315             StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2);
    316             tmp3.append(tmp2);
    317             tmp3.append(':');
    318             tmp3.append(nonce);
    319             tmp3.append(':');
    320             tmp3.append(cnonce);
    321             a1 = tmp3.toString();
    322         } else if (!algorithm.equalsIgnoreCase("MD5")) { // android-changed: ignore case
    323             throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested");
    324         }
    325         String md5a1 = encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
    326 
    327         String a2 = null;
    328         if (qopVariant == QOP_AUTH_INT) {
    329             // Unhandled qop auth-int
    330             //we do not have access to the entity-body or its hash
    331             //TODO: add Method ":" digest-uri-value ":" H(entity-body)
    332         } else {
    333             a2 = method + ':' + uri;
    334         }
    335         String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2)));
    336 
    337         // 3.2.2.1
    338         String serverDigestValue;
    339         if (qopVariant == QOP_MISSING) {
    340             StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + md5a2.length());
    341             tmp2.append(md5a1);
    342             tmp2.append(':');
    343             tmp2.append(nonce);
    344             tmp2.append(':');
    345             tmp2.append(md5a2);
    346             serverDigestValue = tmp2.toString();
    347         } else {
    348             String qopOption = getQopVariantString();
    349             String cnonce = getCnonce();
    350 
    351             StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length()
    352                 + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5);
    353             tmp2.append(md5a1);
    354             tmp2.append(':');
    355             tmp2.append(nonce);
    356             tmp2.append(':');
    357             tmp2.append(NC);
    358             tmp2.append(':');
    359             tmp2.append(cnonce);
    360             tmp2.append(':');
    361             tmp2.append(qopOption);
    362             tmp2.append(':');
    363             tmp2.append(md5a2);
    364             serverDigestValue = tmp2.toString();
    365         }
    366 
    367         String serverDigest =
    368             encode(md5Helper.digest(EncodingUtils.getAsciiBytes(serverDigestValue)));
    369 
    370         return serverDigest;
    371     }
    372 
    373     /**
    374      * Creates digest-response header as defined in RFC2617.
    375      *
    376      * @param credentials User credentials
    377      * @param digest The response tag's value as String.
    378      *
    379      * @return The digest-response as String.
    380      */
    381     private Header createDigestHeader(
    382             final Credentials credentials,
    383             final String digest) throws AuthenticationException {
    384 
    385         CharArrayBuffer buffer = new CharArrayBuffer(128);
    386         if (isProxy()) {
    387             buffer.append(AUTH.PROXY_AUTH_RESP);
    388         } else {
    389             buffer.append(AUTH.WWW_AUTH_RESP);
    390         }
    391         buffer.append(": Digest ");
    392 
    393         String uri = getParameter("uri");
    394         String realm = getParameter("realm");
    395         String nonce = getParameter("nonce");
    396         String opaque = getParameter("opaque");
    397         String response = digest;
    398         String algorithm = getParameter("algorithm");
    399 
    400         String uname = credentials.getUserPrincipal().getName();
    401 
    402         List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
    403         params.add(new BasicNameValuePair("username", uname));
    404         params.add(new BasicNameValuePair("realm", realm));
    405         params.add(new BasicNameValuePair("nonce", nonce));
    406         params.add(new BasicNameValuePair("uri", uri));
    407         params.add(new BasicNameValuePair("response", response));
    408 
    409         if (qopVariant != QOP_MISSING) {
    410             params.add(new BasicNameValuePair("qop", getQopVariantString()));
    411             params.add(new BasicNameValuePair("nc", NC));
    412             params.add(new BasicNameValuePair("cnonce", getCnonce()));
    413         }
    414         if (algorithm != null) {
    415             params.add(new BasicNameValuePair("algorithm", algorithm));
    416         }
    417         if (opaque != null) {
    418             params.add(new BasicNameValuePair("opaque", opaque));
    419         }
    420 
    421         for (int i = 0; i < params.size(); i++) {
    422             BasicNameValuePair param = params.get(i);
    423             if (i > 0) {
    424                 buffer.append(", ");
    425             }
    426             boolean noQuotes = "nc".equals(param.getName()) ||
    427                                "qop".equals(param.getName());
    428             BasicHeaderValueFormatter.DEFAULT
    429                 .formatNameValuePair(buffer, param, !noQuotes);
    430         }
    431         return new BufferedHeader(buffer);
    432     }
    433 
    434     private String getQopVariantString() {
    435         String qopOption;
    436         if (qopVariant == QOP_AUTH_INT) {
    437             qopOption = "auth-int";
    438         } else {
    439             qopOption = "auth";
    440         }
    441         return qopOption;
    442     }
    443 
    444     /**
    445      * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
    446      * <CODE>String</CODE> according to RFC 2617.
    447      *
    448      * @param binaryData array containing the digest
    449      * @return encoded MD5, or <CODE>null</CODE> if encoding failed
    450      */
    451     private static String encode(byte[] binaryData) {
    452         if (binaryData.length != 16) {
    453             return null;
    454         }
    455 
    456         char[] buffer = new char[32];
    457         for (int i = 0; i < 16; i++) {
    458             int low = (binaryData[i] & 0x0f);
    459             int high = ((binaryData[i] & 0xf0) >> 4);
    460             buffer[i * 2] = HEXADECIMAL[high];
    461             buffer[(i * 2) + 1] = HEXADECIMAL[low];
    462         }
    463 
    464         return new String(buffer);
    465     }
    466 
    467 
    468     /**
    469      * Creates a random cnonce value based on the current time.
    470      *
    471      * @return The cnonce value as String.
    472      * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported.
    473      */
    474     public static String createCnonce() {
    475         String cnonce;
    476 
    477         MessageDigest md5Helper = createMessageDigest("MD5");
    478 
    479         cnonce = Long.toString(System.currentTimeMillis());
    480         cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce)));
    481 
    482         return cnonce;
    483     }
    484 }
    485