View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.http.impl.auth;
28  
29  import java.io.IOException;
30  import java.nio.charset.Charset;
31  import java.security.MessageDigest;
32  import java.security.SecureRandom;
33  import java.util.ArrayList;
34  import java.util.Formatter;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.Set;
39  import java.util.StringTokenizer;
40  
41  import org.apache.http.Consts;
42  import org.apache.http.Header;
43  import org.apache.http.HttpEntity;
44  import org.apache.http.HttpEntityEnclosingRequest;
45  import org.apache.http.HttpRequest;
46  import org.apache.http.auth.AUTH;
47  import org.apache.http.auth.AuthenticationException;
48  import org.apache.http.auth.ChallengeState;
49  import org.apache.http.auth.Credentials;
50  import org.apache.http.auth.MalformedChallengeException;
51  import org.apache.http.message.BasicHeaderValueFormatter;
52  import org.apache.http.message.BasicNameValuePair;
53  import org.apache.http.message.BufferedHeader;
54  import org.apache.http.protocol.BasicHttpContext;
55  import org.apache.http.protocol.HttpContext;
56  import org.apache.http.util.Args;
57  import org.apache.http.util.CharArrayBuffer;
58  import org.apache.http.util.EncodingUtils;
59  
60  /**
61   * Digest authentication scheme as defined in RFC 2617.
62   * Both MD5 (default) and MD5-sess are supported.
63   * Currently only qop=auth or no qop is supported. qop=auth-int
64   * is unsupported. If auth and auth-int are provided, auth is
65   * used.
66   * <p>
67   * Since the digest username is included as clear text in the generated
68   * Authentication header, the charset of the username must be compatible
69   * with the HTTP element charset used by the connection.
70   * </p>
71   *
72   * @since 4.0
73   */
74  public class DigestScheme extends RFC2617Scheme {
75  
76      private static final long serialVersionUID = 3883908186234566916L;
77  
78      /**
79       * Hexa values used when creating 32 character long digest in HTTP DigestScheme
80       * in case of authentication.
81       *
82       * @see #encode(byte[])
83       */
84      private static final char[] HEXADECIMAL = {
85          '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
86          'e', 'f'
87      };
88  
89      /** Whether the digest authentication process is complete */
90      private boolean complete;
91  
92      private static final int QOP_UNKNOWN = -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 String lastNonce;
98      private long nounceCount;
99      private String cnonce;
100     private String a1;
101     private String a2;
102 
103     /**
104      * @since 4.3
105      */
106     public DigestScheme(final Charset credentialsCharset) {
107         super(credentialsCharset);
108         this.complete = false;
109     }
110 
111     /**
112      * Creates an instance of {@code DigestScheme} with the given challenge
113      * state.
114      *
115      * @since 4.2
116      *
117      * @deprecated (4.3) do not use.
118      */
119     @Deprecated
120     public DigestScheme(final ChallengeState challengeState) {
121         super(challengeState);
122     }
123 
124     public DigestScheme() {
125         this(Consts.ASCII);
126     }
127 
128     /**
129      * Processes the Digest challenge.
130      *
131      * @param header the challenge header
132      *
133      * @throws MalformedChallengeException is thrown if the authentication challenge
134      * is malformed
135      */
136     @Override
137     public void processChallenge(
138             final Header header) throws MalformedChallengeException {
139         super.processChallenge(header);
140         this.complete = true;
141         if (getParameters().isEmpty()) {
142             throw new MalformedChallengeException("Authentication challenge is empty");
143         }
144     }
145 
146     /**
147      * Tests if the Digest authentication process has been completed.
148      *
149      * @return {@code true} if Digest authorization has been processed,
150      *   {@code false} otherwise.
151      */
152     @Override
153     public boolean isComplete() {
154         final String s = getParameter("stale");
155         return "true".equalsIgnoreCase(s) ? false : this.complete;
156     }
157 
158     /**
159      * Returns textual designation of the digest authentication scheme.
160      *
161      * @return {@code digest}
162      */
163     @Override
164     public String getSchemeName() {
165         return "digest";
166     }
167 
168     /**
169      * Returns {@code false}. Digest authentication scheme is request based.
170      *
171      * @return {@code false}.
172      */
173     @Override
174     public boolean isConnectionBased() {
175         return false;
176     }
177 
178     public void overrideParamter(final String name, final String value) {
179         getParameters().put(name, value);
180     }
181 
182     /**
183      * @deprecated (4.2) Use {@link org.apache.http.auth.ContextAwareAuthScheme#authenticate(
184      *   Credentials, HttpRequest, org.apache.http.protocol.HttpContext)}
185      */
186     @Override
187     @Deprecated
188     public Header authenticate(
189             final Credentials credentials, final HttpRequest request) throws AuthenticationException {
190         return authenticate(credentials, request, new BasicHttpContext());
191     }
192 
193     /**
194      * Produces a digest authorization string for the given set of
195      * {@link Credentials}, method name and URI.
196      *
197      * @param credentials A set of credentials to be used for athentication
198      * @param request    The request being authenticated
199      *
200      * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
201      *         are not valid or not applicable for this authentication scheme
202      * @throws AuthenticationException if authorization string cannot
203      *   be generated due to an authentication failure
204      *
205      * @return a digest authorization string
206      */
207     @Override
208     public Header authenticate(
209             final Credentials credentials,
210             final HttpRequest request,
211             final HttpContext context) throws AuthenticationException {
212 
213         Args.notNull(credentials, "Credentials");
214         Args.notNull(request, "HTTP request");
215         if (getParameter("realm") == null) {
216             throw new AuthenticationException("missing realm in challenge");
217         }
218         if (getParameter("nonce") == null) {
219             throw new AuthenticationException("missing nonce in challenge");
220         }
221         // Add method name and request-URI to the parameter map
222         getParameters().put("methodname", request.getRequestLine().getMethod());
223         getParameters().put("uri", request.getRequestLine().getUri());
224         final String charset = getParameter("charset");
225         if (charset == null) {
226             getParameters().put("charset", getCredentialsCharset(request));
227         }
228         return createDigestHeader(credentials, request);
229     }
230 
231     private static MessageDigest createMessageDigest(
232             final String digAlg) throws UnsupportedDigestAlgorithmException {
233         try {
234             return MessageDigest.getInstance(digAlg);
235         } catch (final Exception e) {
236             throw new UnsupportedDigestAlgorithmException(
237               "Unsupported algorithm in HTTP Digest authentication: "
238                + digAlg);
239         }
240     }
241 
242     /**
243      * Creates digest-response header as defined in RFC2617.
244      *
245      * @param credentials User credentials
246      *
247      * @return The digest-response as String.
248      */
249     private Header createDigestHeader(
250             final Credentials credentials,
251             final HttpRequest request) throws AuthenticationException {
252         final String uri = getParameter("uri");
253         final String realm = getParameter("realm");
254         final String nonce = getParameter("nonce");
255         final String opaque = getParameter("opaque");
256         final String method = getParameter("methodname");
257         String algorithm = getParameter("algorithm");
258         // If an algorithm is not specified, default to MD5.
259         if (algorithm == null) {
260             algorithm = "MD5";
261         }
262 
263         final Set<String> qopset = new HashSet<String>(8);
264         int qop = QOP_UNKNOWN;
265         final String qoplist = getParameter("qop");
266         if (qoplist != null) {
267             final StringTokenizer tok = new StringTokenizer(qoplist, ",");
268             while (tok.hasMoreTokens()) {
269                 final String variant = tok.nextToken().trim();
270                 qopset.add(variant.toLowerCase(Locale.ROOT));
271             }
272             if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) {
273                 qop = QOP_AUTH_INT;
274             } else if (qopset.contains("auth")) {
275                 qop = QOP_AUTH;
276             }
277         } else {
278             qop = QOP_MISSING;
279         }
280 
281         if (qop == QOP_UNKNOWN) {
282             throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
283         }
284 
285         String charset = getParameter("charset");
286         if (charset == null) {
287             charset = "ISO-8859-1";
288         }
289 
290         String digAlg = algorithm;
291         if (digAlg.equalsIgnoreCase("MD5-sess")) {
292             digAlg = "MD5";
293         }
294 
295         final MessageDigest digester;
296         try {
297             digester = createMessageDigest(digAlg);
298         } catch (final UnsupportedDigestAlgorithmException ex) {
299             throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
300         }
301 
302         final String uname = credentials.getUserPrincipal().getName();
303         final String pwd = credentials.getPassword();
304 
305         if (nonce.equals(this.lastNonce)) {
306             nounceCount++;
307         } else {
308             nounceCount = 1;
309             cnonce = null;
310             lastNonce = nonce;
311         }
312         final StringBuilder sb = new StringBuilder(256);
313         final Formatter formatter = new Formatter(sb, Locale.US);
314         formatter.format("%08x", Long.valueOf(nounceCount));
315         formatter.close();
316         final String nc = sb.toString();
317 
318         if (cnonce == null) {
319             cnonce = createCnonce();
320         }
321 
322         a1 = null;
323         a2 = null;
324         // 3.2.2.2: Calculating digest
325         if (algorithm.equalsIgnoreCase("MD5-sess")) {
326             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
327             //      ":" unq(nonce-value)
328             //      ":" unq(cnonce-value)
329 
330             // calculated one per session
331             sb.setLength(0);
332             sb.append(uname).append(':').append(realm).append(':').append(pwd);
333             final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
334             sb.setLength(0);
335             sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
336             a1 = sb.toString();
337         } else {
338             // unq(username-value) ":" unq(realm-value) ":" passwd
339             sb.setLength(0);
340             sb.append(uname).append(':').append(realm).append(':').append(pwd);
341             a1 = sb.toString();
342         }
343 
344         final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
345 
346         if (qop == QOP_AUTH) {
347             // Method ":" digest-uri-value
348             a2 = method + ':' + uri;
349         } else if (qop == QOP_AUTH_INT) {
350             // Method ":" digest-uri-value ":" H(entity-body)
351             HttpEntity entity = null;
352             if (request instanceof HttpEntityEnclosingRequest) {
353                 entity = ((HttpEntityEnclosingRequest) request).getEntity();
354             }
355             if (entity != null && !entity.isRepeatable()) {
356                 // If the entity is not repeatable, try falling back onto QOP_AUTH
357                 if (qopset.contains("auth")) {
358                     qop = QOP_AUTH;
359                     a2 = method + ':' + uri;
360                 } else {
361                     throw new AuthenticationException("Qop auth-int cannot be used with " +
362                             "a non-repeatable entity");
363                 }
364             } else {
365                 final HttpEntityDigesterhtml#HttpEntityDigester">HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
366                 try {
367                     if (entity != null) {
368                         entity.writeTo(entityDigester);
369                     }
370                     entityDigester.close();
371                 } catch (final IOException ex) {
372                     throw new AuthenticationException("I/O error reading entity content", ex);
373                 }
374                 a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
375             }
376         } else {
377             a2 = method + ':' + uri;
378         }
379 
380         final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
381 
382         // 3.2.2.1
383 
384         final String digestValue;
385         if (qop == QOP_MISSING) {
386             sb.setLength(0);
387             sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
388             digestValue = sb.toString();
389         } else {
390             sb.setLength(0);
391             sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
392                 .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
393                 .append(':').append(hasha2);
394             digestValue = sb.toString();
395         }
396 
397         final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
398 
399         final CharArrayBuffer buffer = new CharArrayBuffer(128);
400         if (isProxy()) {
401             buffer.append(AUTH.PROXY_AUTH_RESP);
402         } else {
403             buffer.append(AUTH.WWW_AUTH_RESP);
404         }
405         buffer.append(": Digest ");
406 
407         final 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", digest));
413 
414         if (qop != QOP_MISSING) {
415             params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
416             params.add(new BasicNameValuePair("nc", nc));
417             params.add(new BasicNameValuePair("cnonce", cnonce));
418         }
419         // algorithm cannot be null here
420         params.add(new BasicNameValuePair("algorithm", algorithm));
421         if (opaque != null) {
422             params.add(new BasicNameValuePair("opaque", opaque));
423         }
424 
425         for (int i = 0; i < params.size(); i++) {
426             final BasicNameValuePair param = params.get(i);
427             if (i > 0) {
428                 buffer.append(", ");
429             }
430             final String name = param.getName();
431             final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
432                     || "algorithm".equals(name));
433             BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
434         }
435         return new BufferedHeader(buffer);
436     }
437 
438     String getCnonce() {
439         return cnonce;
440     }
441 
442     String getA1() {
443         return a1;
444     }
445 
446     String getA2() {
447         return a2;
448     }
449 
450     /**
451      * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
452      * <CODE>String</CODE> according to RFC 2617.
453      *
454      * @param binaryData array containing the digest
455      * @return encoded MD5, or <CODE>null</CODE> if encoding failed
456      */
457     static String encode(final byte[] binaryData) {
458         final int n = binaryData.length;
459         final char[] buffer = new char[n * 2];
460         for (int i = 0; i < n; i++) {
461             final int low = (binaryData[i] & 0x0f);
462             final int high = ((binaryData[i] & 0xf0) >> 4);
463             buffer[i * 2] = HEXADECIMAL[high];
464             buffer[(i * 2) + 1] = HEXADECIMAL[low];
465         }
466 
467         return new String(buffer);
468     }
469 
470 
471     /**
472      * Creates a random cnonce value based on the current time.
473      *
474      * @return The cnonce value as String.
475      */
476     public static String createCnonce() {
477         final SecureRandom rnd = new SecureRandom();
478         final byte[] tmp = new byte[8];
479         rnd.nextBytes(tmp);
480         return encode(tmp);
481     }
482 
483     @Override
484     public String toString() {
485         final StringBuilder builder = new StringBuilder();
486         builder.append("DIGEST [complete=").append(complete)
487                 .append(", nonce=").append(lastNonce)
488                 .append(", nc=").append(nounceCount)
489                 .append("]");
490         return builder.toString();
491     }
492 
493 }