View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 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.StandardCharsets;
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.api.Authentication;
37  import org.eclipse.jetty.client.api.ContentResponse;
38  import org.eclipse.jetty.client.api.Request;
39  import org.eclipse.jetty.http.HttpHeader;
40  import org.eclipse.jetty.util.Attributes;
41  import org.eclipse.jetty.util.TypeUtil;
42  
43  /**
44   * Implementation of the HTTP "Digest" authentication defined in RFC 2617.
45   * <p />
46   * Applications should create objects of this class and add them to the
47   * {@link AuthenticationStore} retrieved from the {@link HttpClient}
48   * via {@link HttpClient#getAuthenticationStore()}.
49   */
50  public class DigestAuthentication implements Authentication
51  {
52      private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)");
53  
54      private final URI uri;
55      private final String realm;
56      private final String user;
57      private final String password;
58  
59      /**
60       * @param uri the URI to match for the authentication
61       * @param realm the realm to match for the authentication
62       * @param user the user that wants to authenticate
63       * @param password the password of the user
64       */
65      public DigestAuthentication(URI uri, String realm, String user, String password)
66      {
67          this.uri = uri;
68          this.realm = realm;
69          this.user = user;
70          this.password = password;
71      }
72  
73      @Override
74      public boolean matches(String type, URI uri, String realm)
75      {
76          if (!"digest".equalsIgnoreCase(type))
77              return false;
78  
79          if (!uri.toString().startsWith(this.uri.toString()))
80              return false;
81  
82          return this.realm.equals(realm);
83      }
84  
85      @Override
86      public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context)
87      {
88          Map<String, String> params = parseParameters(headerInfo.getParameters());
89          String nonce = params.get("nonce");
90          if (nonce == null || nonce.length() == 0)
91              return null;
92          String opaque = params.get("opaque");
93          String algorithm = params.get("algorithm");
94          if (algorithm == null)
95              algorithm = "MD5";
96          MessageDigest digester = getMessageDigest(algorithm);
97          if (digester == null)
98              return null;
99          String serverQOP = params.get("qop");
100         String clientQOP = null;
101         if (serverQOP != null)
102         {
103             List<String> serverQOPValues = Arrays.asList(serverQOP.split(","));
104             if (serverQOPValues.contains("auth"))
105                 clientQOP = "auth";
106             else if (serverQOPValues.contains("auth-int"))
107                 clientQOP = "auth-int";
108         }
109 
110         return new DigestResult(headerInfo.getHeader(), uri, response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque);
111     }
112 
113     private Map<String, String> parseParameters(String wwwAuthenticate)
114     {
115         Map<String, String> result = new HashMap<>();
116         List<String> parts = splitParams(wwwAuthenticate);
117         for (String part : parts)
118         {
119             Matcher matcher = PARAM_PATTERN.matcher(part);
120             if (matcher.matches())
121             {
122                 String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH);
123                 String value = matcher.group(2).trim();
124                 if (value.startsWith("\"") && value.endsWith("\""))
125                     value = value.substring(1, value.length() - 1);
126                 result.put(name, value);
127             }
128         }
129         return result;
130     }
131 
132     private List<String> splitParams(String paramString)
133     {
134         List<String> result = new ArrayList<>();
135         int start = 0;
136         for (int i = 0; i < paramString.length(); ++i)
137         {
138             int quotes = 0;
139             char ch = paramString.charAt(i);
140             switch (ch)
141             {
142                 case '\\':
143                     ++i;
144                     break;
145                 case '"':
146                     ++quotes;
147                     break;
148                 case ',':
149                     if (quotes % 2 == 0)
150                     {
151                         String element = paramString.substring(start, i).trim();
152                         if (element.length() > 0)
153                             result.add(element);
154                         start = i + 1;
155                     }
156                     break;
157                 default:
158                     break;
159             }
160         }
161         result.add(paramString.substring(start, paramString.length()).trim());
162         return result;
163     }
164 
165     private MessageDigest getMessageDigest(String algorithm)
166     {
167         try
168         {
169             return MessageDigest.getInstance(algorithm);
170         }
171         catch (NoSuchAlgorithmException x)
172         {
173             return null;
174         }
175     }
176 
177     private class DigestResult implements Result
178     {
179         private final AtomicInteger nonceCount = new AtomicInteger();
180         private final HttpHeader header;
181         private final URI uri;
182         private final byte[] content;
183         private final String realm;
184         private final String user;
185         private final String password;
186         private final String algorithm;
187         private final String nonce;
188         private final String qop;
189         private final String opaque;
190 
191         public DigestResult(HttpHeader header, URI uri, byte[] content, String realm, String user, String password, String algorithm, String nonce, String qop, String opaque)
192         {
193             this.header = header;
194             this.uri = uri;
195             this.content = content;
196             this.realm = realm;
197             this.user = user;
198             this.password = password;
199             this.algorithm = algorithm;
200             this.nonce = nonce;
201             this.qop = qop;
202             this.opaque = opaque;
203         }
204 
205         @Override
206         public URI getURI()
207         {
208             return uri;
209         }
210 
211         @Override
212         public void apply(Request request)
213         {
214             MessageDigest digester = getMessageDigest(algorithm);
215             if (digester == null)
216                 return;
217 
218             String A1 = user + ":" + realm + ":" + password;
219             String hashA1 = toHexString(digester.digest(A1.getBytes(StandardCharsets.ISO_8859_1)));
220 
221             String A2 = request.getMethod() + ":" + request.getURI();
222             if ("auth-int".equals(qop))
223                 A2 += ":" + toHexString(digester.digest(content));
224             String hashA2 = toHexString(digester.digest(A2.getBytes(StandardCharsets.ISO_8859_1)));
225 
226             String nonceCount;
227             String clientNonce;
228             String A3;
229             if (qop != null)
230             {
231                 nonceCount = nextNonceCount();
232                 clientNonce = newClientNonce();
233                 A3 = hashA1 + ":" + nonce + ":" +  nonceCount + ":" + clientNonce + ":" + qop + ":" + hashA2;
234             }
235             else
236             {
237                 nonceCount = null;
238                 clientNonce = null;
239                 A3 = hashA1 + ":" + nonce + ":" + hashA2;
240             }
241             String hashA3 = toHexString(digester.digest(A3.getBytes(StandardCharsets.ISO_8859_1)));
242 
243             StringBuilder value = new StringBuilder("Digest");
244             value.append(" username=\"").append(user).append("\"");
245             value.append(", realm=\"").append(realm).append("\"");
246             value.append(", nonce=\"").append(nonce).append("\"");
247             if (opaque != null)
248                 value.append(", opaque=\"").append(opaque).append("\"");
249             value.append(", algorithm=\"").append(algorithm).append("\"");
250             value.append(", uri=\"").append(request.getURI()).append("\"");
251             if (qop != null)
252             {
253                 value.append(", qop=\"").append(qop).append("\"");
254                 value.append(", nc=\"").append(nonceCount).append("\"");
255                 value.append(", cnonce=\"").append(clientNonce).append("\"");
256             }
257             value.append(", response=\"").append(hashA3).append("\"");
258 
259             request.header(header, value.toString());
260         }
261 
262         private String nextNonceCount()
263         {
264             String padding = "00000000";
265             String next = Integer.toHexString(nonceCount.incrementAndGet()).toLowerCase(Locale.ENGLISH);
266             return padding.substring(0, padding.length() - next.length()) + next;
267         }
268 
269         private String newClientNonce()
270         {
271             Random random = new Random();
272             byte[] bytes = new byte[8];
273             random.nextBytes(bytes);
274             return toHexString(bytes);
275         }
276 
277         private String toHexString(byte[] bytes)
278         {
279             return TypeUtil.toHexString(bytes).toLowerCase(Locale.ENGLISH);
280         }
281     }
282 }