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