View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client.util;
20  
21  import java.net.URI;
22  import java.nio.charset.Charset;
23  import java.security.MessageDigest;
24  import java.security.NoSuchAlgorithmException;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Random;
32  import java.util.concurrent.atomic.AtomicInteger;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import org.eclipse.jetty.client.HttpClient;
37  import org.eclipse.jetty.client.api.Authentication;
38  import org.eclipse.jetty.client.api.AuthenticationStore;
39  import org.eclipse.jetty.client.api.ContentResponse;
40  import org.eclipse.jetty.client.api.Request;
41  import org.eclipse.jetty.http.HttpHeader;
42  import org.eclipse.jetty.util.Attributes;
43  import org.eclipse.jetty.util.TypeUtil;
44  
45  /**
46   * Implementation of the HTTP "Digest" authentication defined in RFC 2617.
47   * <p />
48   * Applications should create objects of this class and add them to the
49   * {@link AuthenticationStore} retrieved from the {@link HttpClient}
50   * via {@link HttpClient#getAuthenticationStore()}.
51   */
52  public class DigestAuthentication implements Authentication
53  {
54      private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
55  
56      private final URI uri;
57      private final String realm;
58      private final String user;
59      private final String password;
60  
61      /**
62       * @param uri the URI to match for the authentication
63       * @param realm the realm to match for the authentication
64       * @param user the user that wants to authenticate
65       * @param password the password of the user
66       */
67      public DigestAuthentication(URI uri, String realm, String user, String password)
68      {
69          this.uri = uri;
70          this.realm = realm;
71          this.user = user;
72          this.password = password;
73      }
74  
75      @Override
76      public boolean matches(String type, URI uri, String realm)
77      {
78          if (!"digest".equalsIgnoreCase(type))
79              return false;
80  
81          if (!uri.toString().startsWith(this.uri.toString()))
82              return false;
83  
84          return this.realm.equals(realm);
85      }
86  
87      @Override
88      public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context)
89      {
90          // Avoid case sensitivity problems on the 'D' character
91          String type = "igest";
92          wwwAuthenticate = wwwAuthenticate.substring(wwwAuthenticate.indexOf(type) + type.length());
93  
94          Map<String, String> params = parseParams(wwwAuthenticate);
95          String nonce = params.get("nonce");
96          if (nonce == null || nonce.length() == 0)
97              return null;
98          String opaque = params.get("opaque");
99          String algorithm = params.get("algorithm");
100         if (algorithm == null)
101             algorithm = "MD5";
102         MessageDigest digester = getMessageDigest(algorithm);
103         if (digester == null)
104             return null;
105         String serverQOP = params.get("qop");
106         String clientQOP = null;
107         if (serverQOP != null)
108         {
109             List<String> serverQOPValues = Arrays.asList(serverQOP.split(","));
110             if (serverQOPValues.contains("auth"))
111                 clientQOP = "auth";
112             else if (serverQOPValues.contains("auth-int"))
113                 clientQOP = "auth-int";
114         }
115 
116         return new DigestResult(request.getURI(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque);
117     }
118 
119     private Map<String, String> parseParams(String wwwAuthenticate)
120     {
121         Map<String, String> result = new HashMap<>();
122         List<String> parts = splitParams(wwwAuthenticate);
123         for (String part : parts)
124         {
125             Matcher matcher = PARAM_PATTERN.matcher(part);
126             if (matcher.matches())
127             {
128                 String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
129                 String value = matcher.group(2).trim();
130                 if (value.startsWith("\"") && value.endsWith("\""))
131                     value = value.substring(1, value.length() - 1);
132                 result.put(name, value);
133             }
134         }
135         return result;
136     }
137 
138     private List<String> splitParams(String paramString)
139     {
140         List<String> result = new ArrayList<>();
141         int start = 0;
142         for (int i = 0; i < paramString.length(); ++i)
143         {
144             int quotes = 0;
145             char ch = paramString.charAt(i);
146             switch (ch)
147             {
148                 case '\\':
149                     ++i;
150                     break;
151                 case '"':
152                     ++quotes;
153                     break;
154                 case ',':
155                     if (quotes % 2 == 0)
156                     {
157                         result.add(paramString.substring(start, i).trim());
158                         start = i + 1;
159                     }
160                     break;
161                 default:
162                     break;
163             }
164         }
165         result.add(paramString.substring(start, paramString.length()).trim());
166         return result;
167     }
168 
169     private MessageDigest getMessageDigest(String algorithm)
170     {
171         try
172         {
173             return MessageDigest.getInstance(algorithm);
174         }
175         catch (NoSuchAlgorithmException x)
176         {
177             return null;
178         }
179     }
180 
181     private class DigestResult implements Result
182     {
183         private final AtomicInteger nonceCount = new AtomicInteger();
184         private final URI uri;
185         private final byte[] content;
186         private final String realm;
187         private final String user;
188         private final String password;
189         private final String algorithm;
190         private final String nonce;
191         private final String qop;
192         private final String opaque;
193 
194         public DigestResult(URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
195         {
196             this.uri = uri;
197             this.content = content;
198             this.realm = realm;
199             this.user = user;
200             this.password = password;
201             this.algorithm = algorithm;
202             this.nonce = nonce;
203             this.qop = qop;
204             this.opaque = opaque;
205         }
206 
207         @Override
208         public URI getURI()
209         {
210             return uri;
211         }
212 
213         @Override
214         public void apply(Request request)
215         {
216             if (!request.getURI().toString().startsWith(uri.toString()))
217                 return;
218 
219             MessageDigest digester = getMessageDigest(algorithm);
220             if (digester == null)
221                 return;
222 
223             Charset charset = Charset.forName("ISO-8859-1");
224             String A1 = user + ":" + realm + ":" + password;
225             String hashA1 = toHexString(digester.digest(A1.getBytes(charset)));
226 
227             String A2 = request.getMethod().asString() + ":" + request.getURI();
228             if ("auth-int".equals(qop))
229                 A2 += ":" + toHexString(digester.digest(content));
230             String hashA2 = toHexString(digester.digest(A2.getBytes(charset)));
231 
232             String nonceCount;
233             String clientNonce;
234             String A3;
235             if (qop != null)
236             {
237                 nonceCount = nextNonceCount();
238                 clientNonce = newClientNonce();
239                 A3 = hashA1 + ":" + nonce + ":" +  nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
240             }
241             else
242             {
243                 nonceCount = null;
244                 clientNonce = null;
245                 A3 = hashA1 + ":" + nonce + ":" + hashA2;
246             }
247             String hashA3 = toHexString(digester.digest(A3.getBytes(charset)));
248 
249             StringBuilder value = new StringBuilder("Digest");
250             value.append(" username=\"").append(user).append("\"");
251             value.append(", realm=\"").append(realm).append("\"");
252             value.append(", nonce=\"").append(nonce).append("\"");
253             if (opaque != null)
254                 value.append(", opaque=\"").append(opaque).append("\"");
255             value.append(", algorithm=\"").append(algorithm).append("\"");
256             value.append(", uri=\"").append(request.getURI()).append("\"");
257             if (qop != null)
258             {
259                 value.append(", qop=\"").append(qop).append("\"");
260                 value.append(", nc=\"").append(nonceCount).append("\"");
261                 value.append(", cnonce=\"").append(clientNonce).append("\"");
262             }
263             value.append(", response=\"").append(hashA3).append("\"");
264 
265             request.header(HttpHeader.AUTHORIZATION.asString(), value.toString());
266         }
267 
268         private String nextNonceCount()
269         {
270             String padding = "00000000";
271             String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase(Locale.ENGLISH);
272             return padding.substring(0, padding.length() - next.length()) + next;
273         }
274 
275         private String newClientNonce()
276         {
277             Random random = new Random();
278             byte[] bytes = new byte[8];
279             random.nextBytes(bytes);
280             return toHexString(bytes);
281         }
282 
283         private String toHexString(byte[] bytes)
284         {
285             return TypeUtil.toHexString(bytes).toLowerCase(Locale.ENGLISH);
286         }
287     }
288 }