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