View Javadoc

1   /*
2    * ====================================================================
3    *
4    *  Licensed to the Apache Software Foundation (ASF) under one or more
5    *  contributor license agreements.  See the NOTICE file distributed with
6    *  this work for additional information regarding copyright ownership.
7    *  The ASF licenses this file to You under the Apache License, Version 2.0
8    *  (the "License"); you may not use this file except in compliance with
9    *  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, software
14   *  distributed under the License is distributed on an "AS IS" BASIS,
15   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   *  See the License for the specific language governing permissions and
17   *  limitations under the License.
18   * ====================================================================
19   *
20   * This software consists of voluntary contributions made by many
21   * individuals on behalf of the Apache Software Foundation.  For more
22   * information on the Apache Software Foundation, please see
23   * <http://www.apache.org/>.
24   *
25   */
26  
27  package org.apache.http.impl.auth;
28  
29  import java.io.IOException;
30  import java.security.MessageDigest;
31  import java.security.SecureRandom;
32  import java.util.ArrayList;
33  import java.util.Formatter;
34  import java.util.HashSet;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Set;
38  import java.util.StringTokenizer;
39  
40  import org.apache.http.annotation.NotThreadSafe;
41  
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.AuthenticationException;
47  import org.apache.http.auth.ChallengeState;
48  import org.apache.http.auth.ContextAwareAuthScheme;
49  import org.apache.http.auth.Credentials;
50  import org.apache.http.auth.AUTH;
51  import org.apache.http.auth.MalformedChallengeException;
52  import org.apache.http.auth.params.AuthParams;
53  import org.apache.http.message.BasicNameValuePair;
54  import org.apache.http.message.BasicHeaderValueFormatter;
55  import org.apache.http.message.BufferedHeader;
56  import org.apache.http.protocol.BasicHttpContext;
57  import org.apache.http.protocol.HttpContext;
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   * Credential charset is configured via the
69   * {@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET}
70   * parameter of the HTTP request.
71   * <p>
72   * Since the digest username is included as clear text in the generated
73   * Authentication header, the charset of the username must be compatible
74   * with the
75   * {@link org.apache.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET
76   *        http element charset}.
77   * <p>
78   * The following parameters can be used to customize the behavior of this
79   * class:
80   * <ul>
81   *  <li>{@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li>
82   * </ul>
83   *
84   * @since 4.0
85   */
86  @NotThreadSafe
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     private static final int QOP_UNKNOWN = -1;
104     private static final int QOP_MISSING = 0;
105     private static final int QOP_AUTH_INT = 1;
106     private static final int QOP_AUTH = 2;
107 
108     private String lastNonce;
109     private long nounceCount;
110     private String cnonce;
111     private String a1;
112     private String a2;
113 
114     /**
115      * Creates an instance of <tt>DigestScheme</tt> with the given challenge
116      * state.
117      *
118      * @since 4.2
119      */
120     public DigestScheme(final ChallengeState challengeState) {
121         super(challengeState);
122         this.complete = false;
123     }
124 
125     public DigestScheme() {
126         this(null);
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 <tt>true</tt> if Digest authorization has been processed,
148      *   <tt>false</tt> otherwise.
149      */
150     public boolean isComplete() {
151         String s = getParameter("stale");
152         if ("true".equalsIgnoreCase(s)) {
153             return false;
154         } else {
155             return this.complete;
156         }
157     }
158 
159     /**
160      * Returns textual designation of the digest authentication scheme.
161      *
162      * @return <code>digest</code>
163      */
164     public String getSchemeName() {
165         return "digest";
166     }
167 
168     /**
169      * Returns <tt>false</tt>. Digest authentication scheme is request based.
170      *
171      * @return <tt>false</tt>.
172      */
173     public boolean isConnectionBased() {
174         return false;
175     }
176 
177     public void overrideParamter(final String name, final String value) {
178         getParameters().put(name, value);
179     }
180 
181     /**
182      * @deprecated (4.2) Use {@link ContextAwareAuthScheme#authenticate(Credentials, HttpRequest, org.apache.http.protocol.HttpContext)}
183      */
184     @Deprecated 
185     public Header authenticate(
186             final Credentials credentials, final HttpRequest request) throws AuthenticationException {
187         return authenticate(credentials, request, new BasicHttpContext());
188     }
189 
190     /**
191      * Produces a digest authorization string for the given set of
192      * {@link Credentials}, method name and URI.
193      *
194      * @param credentials A set of credentials to be used for athentication
195      * @param request    The request being authenticated
196      *
197      * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
198      *         are not valid or not applicable for this authentication scheme
199      * @throws AuthenticationException if authorization string cannot
200      *   be generated due to an authentication failure
201      *
202      * @return a digest authorization string
203      */
204     @Override
205     public Header authenticate(
206             final Credentials credentials,
207             final HttpRequest request,
208             final HttpContext context) throws AuthenticationException {
209 
210         if (credentials == null) {
211             throw new IllegalArgumentException("Credentials may not be null");
212         }
213         if (request == null) {
214             throw new IllegalArgumentException("HTTP request may not be null");
215         }
216         if (getParameter("realm") == null) {
217             throw new AuthenticationException("missing realm in challenge");
218         }
219         if (getParameter("nonce") == null) {
220             throw new AuthenticationException("missing nonce in challenge");
221         }
222         // Add method name and request-URI to the parameter map
223         getParameters().put("methodname", request.getRequestLine().getMethod());
224         getParameters().put("uri", request.getRequestLine().getUri());
225         String charset = getParameter("charset");
226         if (charset == null) {
227             charset = AuthParams.getCredentialCharset(request.getParams());
228             getParameters().put("charset", charset);
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 (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         String uri = getParameter("uri");
255         String realm = getParameter("realm");
256         String nonce = getParameter("nonce");
257         String opaque = getParameter("opaque");
258         String method = getParameter("methodname");
259         String algorithm = getParameter("algorithm");
260 
261         Set<String> qopset = new HashSet<String>(8);
262         int qop = QOP_UNKNOWN;
263         String qoplist = getParameter("qop");
264         if (qoplist != null) {
265             StringTokenizer tok = new StringTokenizer(qoplist, ",");
266             while (tok.hasMoreTokens()) {
267                 String variant = tok.nextToken().trim();
268                 qopset.add(variant.toLowerCase(Locale.US));
269             }
270             if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) {
271                 qop = QOP_AUTH_INT;
272             } else if (qopset.contains("auth")) {
273                 qop = QOP_AUTH;
274             }
275         } else {
276             qop = QOP_MISSING;
277         }
278 
279         if (qop == QOP_UNKNOWN) {
280             throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
281         }
282 
283         // If an algorithm is not specified, default to MD5.
284         if (algorithm == null) {
285             algorithm = "MD5";
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         MessageDigest digester;
298         try {
299             digester = createMessageDigest(digAlg);
300         } catch (UnsupportedDigestAlgorithmException ex) {
301             throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
302         }
303 
304         String uname = credentials.getUserPrincipal().getName();
305         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         StringBuilder sb = new StringBuilder(256);
315         Formatter formatter = new Formatter(sb, Locale.US);
316         formatter.format("%08x", nounceCount);
317         String nc = sb.toString();
318 
319         if (cnonce == null) {
320             cnonce = createCnonce();
321         }
322 
323         a1 = null;
324         a2 = null;
325         // 3.2.2.2: Calculating digest
326         if (algorithm.equalsIgnoreCase("MD5-sess")) {
327             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
328             //      ":" unq(nonce-value)
329             //      ":" unq(cnonce-value)
330 
331             // calculated one per session
332             sb.setLength(0);
333             sb.append(uname).append(':').append(realm).append(':').append(pwd);
334             String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
335             sb.setLength(0);
336             sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
337             a1 = sb.toString();
338         } else {
339             // unq(username-value) ":" unq(realm-value) ":" passwd
340             sb.setLength(0);
341             sb.append(uname).append(':').append(realm).append(':').append(pwd);
342             a1 = sb.toString();
343         }
344 
345         String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
346 
347         if (qop == QOP_AUTH) {
348             // Method ":" digest-uri-value
349             a2 = method + ':' + uri;
350         } else if (qop == QOP_AUTH_INT) {
351             // Method ":" digest-uri-value ":" H(entity-body)
352             HttpEntity entity = null;
353             if (request instanceof HttpEntityEnclosingRequest) {
354                 entity = ((HttpEntityEnclosingRequest) request).getEntity();
355             }
356             if (entity != null && !entity.isRepeatable()) {
357                 // If the entity is not repeatable, try falling back onto QOP_AUTH
358                 if (qopset.contains("auth")) {
359                     qop = QOP_AUTH;
360                     a2 = method + ':' + uri;
361                 } else {
362                     throw new AuthenticationException("Qop auth-int cannot be used with " +
363                             "a non-repeatable entity");
364                 }
365             } else {
366                 HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
367                 try {
368                     if (entity != null) {
369                         entity.writeTo(entityDigester);
370                     }
371                     entityDigester.close();
372                 } catch (IOException ex) {
373                     throw new AuthenticationException("I/O error reading entity content", ex);
374                 }
375                 a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
376             }
377         } else {
378             a2 = method + ':' + uri;
379         }
380 
381         String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
382 
383         // 3.2.2.1
384 
385         String digestValue;
386         if (qop == QOP_MISSING) {
387             sb.setLength(0);
388             sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
389             digestValue = sb.toString();
390         } else {
391             sb.setLength(0);
392             sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
393                 .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
394                 .append(':').append(hasha2);
395             digestValue = sb.toString();
396         }
397 
398         String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
399 
400         CharArrayBuffer buffer = new CharArrayBuffer(128);
401         if (isProxy()) {
402             buffer.append(AUTH.PROXY_AUTH_RESP);
403         } else {
404             buffer.append(AUTH.WWW_AUTH_RESP);
405         }
406         buffer.append(": Digest ");
407 
408         List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
409         params.add(new BasicNameValuePair("username", uname));
410         params.add(new BasicNameValuePair("realm", realm));
411         params.add(new BasicNameValuePair("nonce", nonce));
412         params.add(new BasicNameValuePair("uri", uri));
413         params.add(new BasicNameValuePair("response", digest));
414 
415         if (qop != QOP_MISSING) {
416             params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
417             params.add(new BasicNameValuePair("nc", nc));
418             params.add(new BasicNameValuePair("cnonce", cnonce));
419         }
420         if (algorithm != null) {
421             params.add(new BasicNameValuePair("algorithm", algorithm));
422         }
423         if (opaque != null) {
424             params.add(new BasicNameValuePair("opaque", opaque));
425         }
426 
427         for (int i = 0; i < params.size(); i++) {
428             BasicNameValuePair param = params.get(i);
429             if (i > 0) {
430                 buffer.append(", ");
431             }
432             boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName());
433             BasicHeaderValueFormatter.DEFAULT.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(byte[] binaryData) {
458         int n = binaryData.length;
459         char[] buffer = new char[n * 2];
460         for (int i = 0; i < n; i++) {
461             int low = (binaryData[i] & 0x0f);
462             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         SecureRandom rnd = new SecureRandom();
478         byte[] tmp = new byte[8];
479         rnd.nextBytes(tmp);
480         return encode(tmp);
481     }
482 
483     @Override
484     public String toString() {
485         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 }