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