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