View Javadoc

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