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  * @deprecated Please use {@link java.net.URL#openConnection} instead.
     82  *     Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a>
     83  *     for further details.
     84  */
     85 
     86 @Deprecated
     87 public class DigestScheme extends RFC2617Scheme {
     88 
     89     /**
     90      * Hexa values used when creating 32 character long digest in HTTP DigestScheme
     91      * in case of authentication.
     92      *
     93      * @see #encode(byte[])
     94      */
     95     private static final char[] HEXADECIMAL = {
     96         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
     97         'e', 'f'
     98     };
     99 
    100     /** Whether the digest authentication process is complete */
    101     private boolean complete;
    102 
    103     //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay
    104     private static final String NC = "00000001"; //nonce-count is always 1
    105     private static final int QOP_MISSING = 0;
    106     private static final int QOP_AUTH_INT = 1;
    107     private static final int QOP_AUTH = 2;
    108 
    109     private int qopVariant = QOP_MISSING;
    110     private String cnonce;
    111 
    112     /**
    113      * Default constructor for the digest authetication scheme.
    114      */
    115     public DigestScheme() {
    116         super();
    117         this.complete = false;
    118     }
    119 
    120     /**
    121      * Processes the Digest challenge.
    122      *
    123      * @param header the challenge header
    124      *
    125      * @throws MalformedChallengeException is thrown if the authentication challenge
    126      * is malformed
    127      */
    128     @Override
    129     public void processChallenge(
    130             final Header header) throws MalformedChallengeException {
    131         super.processChallenge(header);
    132 
    133         if (getParameter("realm") == null) {
    134             throw new MalformedChallengeException("missing realm in challange");
    135         }
    136         if (getParameter("nonce") == null) {
    137             throw new MalformedChallengeException("missing nonce in challange");
    138         }
    139 
    140         boolean unsupportedQop = false;
    141         // qop parsing
    142         String qop = getParameter("qop");
    143         if (qop != null) {
    144             StringTokenizer tok = new StringTokenizer(qop,",");
    145             while (tok.hasMoreTokens()) {
    146                 String variant = tok.nextToken().trim();
    147                 if (variant.equals("auth")) {
    148                     qopVariant = QOP_AUTH;
    149                     break; //that's our favourite, because auth-int is unsupported
    150                 } else if (variant.equals("auth-int")) {
    151                     qopVariant = QOP_AUTH_INT;
    152                 } else {
    153                     unsupportedQop = true;
    154                 }
    155             }
    156         }
    157 
    158         if (unsupportedQop && (qopVariant == QOP_MISSING)) {
    159             throw new MalformedChallengeException("None of the qop methods is supported");
    160         }
    161         // Reset cnonce
    162         this.cnonce = null;
    163         this.complete = true;
    164     }
    165 
    166     /**
    167      * Tests if the Digest authentication process has been completed.
    168      *
    169      * @return <tt>true</tt> if Digest authorization has been processed,
    170      *   <tt>false</tt> otherwise.
    171      */
    172     public boolean isComplete() {
    173         String s = getParameter("stale");
    174         if ("true".equalsIgnoreCase(s)) {
    175             return false;
    176         } else {
    177             return this.complete;
    178         }
    179     }
    180 
    181     /**
    182      * Returns textual designation of the digest authentication scheme.
    183      *
    184      * @return <code>digest</code>
    185      */
    186     public String getSchemeName() {
    187         return "digest";
    188     }
    189 
    190     /**
    191      * Returns <tt>false</tt>. Digest authentication scheme is request based.
    192      *
    193      * @return <tt>false</tt>.
    194      */
    195     public boolean isConnectionBased() {
    196         return false;
    197     }
    198 
    199     public void overrideParamter(final String name, final String value) {
    200         getParameters().put(name, value);
    201     }
    202 
    203     private String getCnonce() {
    204         if (this.cnonce == null) {
    205             this.cnonce = createCnonce();
    206         }
    207         return this.cnonce;
    208     }
    209 
    210     /**
    211      * Produces a digest authorization string for the given set of
    212      * {@link Credentials}, method name and URI.
    213      *
    214      * @param credentials A set of credentials to be used for athentication
    215      * @param request    The request being authenticated
    216      *
    217      * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
    218      *         are not valid or not applicable for this authentication scheme
    219      * @throws AuthenticationException if authorization string cannot
    220      *   be generated due to an authentication failure
    221      *
    222      * @return a digest authorization string
    223      */
    224     public Header authenticate(
    225             final Credentials credentials,
    226             final HttpRequest request) throws AuthenticationException {
    227 
    228         if (credentials == null) {
    229             throw new IllegalArgumentException("Credentials may not be null");
    230         }
    231         if (request == null) {
    232             throw new IllegalArgumentException("HTTP request may not be null");
    233         }
    234 
    235         // Add method name and request-URI to the parameter map
    236         getParameters().put("methodname", request.getRequestLine().getMethod());
    237         getParameters().put("uri", request.getRequestLine().getUri());
    238         String charset = getParameter("charset");
    239         if (charset == null) {
    240             charset = AuthParams.getCredentialCharset(request.getParams());
    241             getParameters().put("charset", charset);
    242         }
    243         String digest = createDigest(credentials);
    244         return createDigestHeader(credentials, digest);
    245     }
    246 
    247     private static MessageDigest createMessageDigest(
    248             final String digAlg) throws UnsupportedDigestAlgorithmException {
    249         try {
    250             return MessageDigest.getInstance(digAlg);
    251         } catch (Exception e) {
    252             throw new UnsupportedDigestAlgorithmException(
    253               "Unsupported algorithm in HTTP Digest authentication: "
    254                + digAlg);
    255         }
    256     }
    257 
    258     /**
    259      * Creates an MD5 response digest.
    260      *
    261      * @return The created digest as string. This will be the response tag's
    262      *         value in the Authentication HTTP header.
    263      * @throws AuthenticationException when MD5 is an unsupported algorithm
    264      */
    265     private String createDigest(final Credentials credentials) throws AuthenticationException {
    266         // Collecting required tokens
    267         String uri = getParameter("uri");
    268         String realm = getParameter("realm");
    269         String nonce = getParameter("nonce");
    270         String method = getParameter("methodname");
    271         String algorithm = getParameter("algorithm");
    272         if (uri == null) {
    273             throw new IllegalStateException("URI may not be null");
    274         }
    275         if (realm == null) {
    276             throw new IllegalStateException("Realm may not be null");
    277         }
    278         if (nonce == null) {
    279             throw new IllegalStateException("Nonce may not be null");
    280         }
    281         // If an algorithm is not specified, default to MD5.
    282         if (algorithm == null) {
    283             algorithm = "MD5";
    284         }
    285         // If an charset is not specified, default to ISO-8859-1.
    286         String charset = getParameter("charset");
    287         if (charset == null) {
    288             charset = "ISO-8859-1";
    289         }
    290 
    291         if (qopVariant == QOP_AUTH_INT) {
    292             throw new AuthenticationException(
    293                 "Unsupported qop in HTTP Digest authentication");
    294         }
    295 
    296         MessageDigest md5Helper = createMessageDigest("MD5");
    297 
    298         String uname = credentials.getUserPrincipal().getName();
    299         String pwd = credentials.getPassword();
    300 
    301         // 3.2.2.2: Calculating digest
    302         StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2);
    303         tmp.append(uname);
    304         tmp.append(':');
    305         tmp.append(realm);
    306         tmp.append(':');
    307         tmp.append(pwd);
    308         // unq(username-value) ":" unq(realm-value) ":" passwd
    309         String a1 = tmp.toString();
    310 
    311         //a1 is suitable for MD5 algorithm
    312         if(algorithm.equalsIgnoreCase("MD5-sess")) { // android-changed: ignore case
    313             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
    314             //      ":" unq(nonce-value)
    315             //      ":" unq(cnonce-value)
    316 
    317             String cnonce = getCnonce();
    318 
    319             String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
    320             StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2);
    321             tmp3.append(tmp2);
    322             tmp3.append(':');
    323             tmp3.append(nonce);
    324             tmp3.append(':');
    325             tmp3.append(cnonce);
    326             a1 = tmp3.toString();
    327         } else if (!algorithm.equalsIgnoreCase("MD5")) { // android-changed: ignore case
    328             throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested");
    329         }
    330         String md5a1 = encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
    331 
    332         String a2 = null;
    333         if (qopVariant == QOP_AUTH_INT) {
    334             // Unhandled qop auth-int
    335             //we do not have access to the entity-body or its hash
    336             //TODO: add Method ":" digest-uri-value ":" H(entity-body)
    337         } else {
    338             a2 = method + ':' + uri;
    339         }
    340         String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2)));
    341 
    342         // 3.2.2.1
    343         String serverDigestValue;
    344         if (qopVariant == QOP_MISSING) {
    345             StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + md5a2.length());
    346             tmp2.append(md5a1);
    347             tmp2.append(':');
    348             tmp2.append(nonce);
    349             tmp2.append(':');
    350             tmp2.append(md5a2);
    351             serverDigestValue = tmp2.toString();
    352         } else {
    353             String qopOption = getQopVariantString();
    354             String cnonce = getCnonce();
    355 
    356             StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length()
    357                 + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5);
    358             tmp2.append(md5a1);
    359             tmp2.append(':');
    360             tmp2.append(nonce);
    361             tmp2.append(':');
    362             tmp2.append(NC);
    363             tmp2.append(':');
    364             tmp2.append(cnonce);
    365             tmp2.append(':');
    366             tmp2.append(qopOption);
    367             tmp2.append(':');
    368             tmp2.append(md5a2);
    369             serverDigestValue = tmp2.toString();
    370         }
    371 
    372         String serverDigest =
    373             encode(md5Helper.digest(EncodingUtils.getAsciiBytes(serverDigestValue)));
    374 
    375         return serverDigest;
    376     }
    377 
    378     /**
    379      * Creates digest-response header as defined in RFC2617.
    380      *
    381      * @param credentials User credentials
    382      * @param digest The response tag's value as String.
    383      *
    384      * @return The digest-response as String.
    385      */
    386     private Header createDigestHeader(
    387             final Credentials credentials,
    388             final String digest) throws AuthenticationException {
    389 
    390         CharArrayBuffer buffer = new CharArrayBuffer(128);
    391         if (isProxy()) {
    392             buffer.append(AUTH.PROXY_AUTH_RESP);
    393         } else {
    394             buffer.append(AUTH.WWW_AUTH_RESP);
    395         }
    396         buffer.append(": Digest ");
    397 
    398         String uri = getParameter("uri");
    399         String realm = getParameter("realm");
    400         String nonce = getParameter("nonce");
    401         String opaque = getParameter("opaque");
    402         String response = digest;
    403         String algorithm = getParameter("algorithm");
    404 
    405         String uname = credentials.getUserPrincipal().getName();
    406 
    407         List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
    408         params.add(new BasicNameValuePair("username", uname));
    409         params.add(new BasicNameValuePair("realm", realm));
    410         params.add(new BasicNameValuePair("nonce", nonce));
    411         params.add(new BasicNameValuePair("uri", uri));
    412         params.add(new BasicNameValuePair("response", response));
    413 
    414         if (qopVariant != QOP_MISSING) {
    415             params.add(new BasicNameValuePair("qop", getQopVariantString()));
    416             params.add(new BasicNameValuePair("nc", NC));
    417             params.add(new BasicNameValuePair("cnonce", getCnonce()));
    418         }
    419         if (algorithm != null) {
    420             params.add(new BasicNameValuePair("algorithm", algorithm));
    421         }
    422         if (opaque != null) {
    423             params.add(new BasicNameValuePair("opaque", opaque));
    424         }
    425 
    426         for (int i = 0; i < params.size(); i++) {
    427             BasicNameValuePair param = params.get(i);
    428             if (i > 0) {
    429                 buffer.append(", ");
    430             }
    431             boolean noQuotes = "nc".equals(param.getName()) ||
    432                                "qop".equals(param.getName());
    433             BasicHeaderValueFormatter.DEFAULT
    434                 .formatNameValuePair(buffer, param, !noQuotes);
    435         }
    436         return new BufferedHeader(buffer);
    437     }
    438 
    439     private String getQopVariantString() {
    440         String qopOption;
    441         if (qopVariant == QOP_AUTH_INT) {
    442             qopOption = "auth-int";
    443         } else {
    444             qopOption = "auth";
    445         }
    446         return qopOption;
    447     }
    448 
    449     /**
    450      * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
    451      * <CODE>String</CODE> according to RFC 2617.
    452      *
    453      * @param binaryData array containing the digest
    454      * @return encoded MD5, or <CODE>null</CODE> if encoding failed
    455      */
    456     private static String encode(byte[] binaryData) {
    457         if (binaryData.length != 16) {
    458             return null;
    459         }
    460 
    461         char[] buffer = new char[32];
    462         for (int i = 0; i < 16; i++) {
    463             int low = (binaryData[i] & 0x0f);
    464             int high = ((binaryData[i] & 0xf0) >> 4);
    465             buffer[i * 2] = HEXADECIMAL[high];
    466             buffer[(i * 2) + 1] = HEXADECIMAL[low];
    467         }
    468 
    469         return new String(buffer);
    470     }
    471 
    472 
    473     /**
    474      * Creates a random cnonce value based on the current time.
    475      *
    476      * @return The cnonce value as String.
    477      * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported.
    478      */
    479     public static String createCnonce() {
    480         String cnonce;
    481 
    482         MessageDigest md5Helper = createMessageDigest("MD5");
    483 
    484         cnonce = Long.toString(System.currentTimeMillis());
    485         cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce)));
    486 
    487         return cnonce;
    488     }
    489 }
    490