View Javadoc
1   /*
2    * Copyright 2008-2019 Web Cohesion
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.security.oauth.consumer.client;
18  
19  import org.apache.commons.codec.DecoderException;
20  import org.apache.commons.logging.Log;
21  import org.apache.commons.logging.LogFactory;
22  import org.springframework.beans.factory.InitializingBean;
23  import org.springframework.beans.factory.annotation.Autowired;
24  import org.springframework.security.oauth.common.OAuthCodec;
25  import org.springframework.security.oauth.common.OAuthConsumerParameter;
26  import org.springframework.security.oauth.common.OAuthProviderParameter;
27  import org.springframework.security.oauth.common.StringSplitUtils;
28  import org.springframework.security.oauth.common.signature.CoreOAuthSignatureMethodFactory;
29  import org.springframework.security.oauth.common.signature.OAuthSignatureMethod;
30  import org.springframework.security.oauth.common.signature.OAuthSignatureMethodFactory;
31  import org.springframework.security.oauth.common.signature.UnsupportedSignatureMethodException;
32  import org.springframework.security.oauth.consumer.InvalidOAuthRealmException;
33  import org.springframework.security.oauth.consumer.OAuthConsumerSupport;
34  import org.springframework.security.oauth.consumer.OAuthConsumerToken;
35  import org.springframework.security.oauth.consumer.OAuthRequestFailedException;
36  import org.springframework.security.oauth.consumer.ProtectedResourceDetails;
37  import org.springframework.security.oauth.consumer.ProtectedResourceDetailsService;
38  import org.springframework.security.oauth.consumer.UnverifiedRequestTokenException;
39  import org.springframework.security.oauth.consumer.net.OAuthURLStreamHandlerFactory;
40  import org.springframework.security.oauth.consumer.nonce.NonceFactory;
41  import org.springframework.security.oauth.consumer.nonce.UUIDNonceFactory;
42  import org.springframework.util.Assert;
43  
44  import java.io.*;
45  import java.net.*;
46  import java.util.*;
47  
48  import static org.springframework.security.oauth.common.OAuthCodec.oauthEncode;
49  
50  /**
51   * Consumer-side support for OAuth. This support uses a {@link java.net.URLConnection} to interface with the
52   * OAuth provider.  A proxy will be selected, but it is assumed that the {@link javax.net.ssl.TrustManager}s
53   * and other connection-related environment variables are already set up.
54   *
55   * @author Ryan Heaton
56   * @author Andrew McCall
57   */
58  public class CoreOAuthConsumerSupport implements OAuthConsumerSupport, InitializingBean {
59  
60    private static final Log logger = LogFactory.getLog(CoreOAuthConsumerSupport.class);
61    private OAuthURLStreamHandlerFactory streamHandlerFactory;
62    private OAuthSignatureMethodFactory signatureFactory = new CoreOAuthSignatureMethodFactory();
63    private NonceFactory nonceFactory = new UUIDNonceFactory();
64  
65    private ProtectedResourceDetailsService protectedResourceDetailsService;
66  
67    private ProxySelector proxySelector = ProxySelector.getDefault();
68    private int connectionTimeout = 1000 * 60;
69    private int readTimeout = 1000 * 60;
70  
71    public CoreOAuthConsumerSupport() {
72      try {
73        this.streamHandlerFactory = ((OAuthURLStreamHandlerFactory) Class.forName("org.springframework.security.oauth.consumer.net.DefaultOAuthURLStreamHandlerFactory").newInstance());
74      }
75      catch (Throwable e) {
76        throw new IllegalStateException(e);
77      }
78    }
79  
80    public CoreOAuthConsumerSupport(OAuthURLStreamHandlerFactory streamHandlerFactory) {
81      this.streamHandlerFactory = streamHandlerFactory;
82    }
83  
84    public void afterPropertiesSet() throws Exception {
85      Assert.notNull(protectedResourceDetailsService, "A protected resource details service is required.");
86      Assert.notNull(streamHandlerFactory, "A stream handler factory is required.");
87    }
88  
89    // Inherited.
90    public OAuthConsumerToken getUnauthorizedRequestToken(String resourceId, String callback) throws OAuthRequestFailedException {
91      ProtectedResourceDetails details = getProtectedResourceDetailsService().loadProtectedResourceDetailsById(resourceId);
92      return getUnauthorizedRequestToken(details, callback);
93    }
94    
95    public OAuthConsumerToken getUnauthorizedRequestToken(ProtectedResourceDetails details, String callback) throws OAuthRequestFailedException {  
96      URL requestTokenURL;
97      try {
98        requestTokenURL = new URL(details.getRequestTokenURL());
99      }
100     catch (MalformedURLException e) {
101       throw new IllegalStateException("Malformed URL for obtaining a request token.", e);
102     }
103 
104     String httpMethod = details.getRequestTokenHttpMethod();
105 
106     Map<String, String> additionalParameters = new TreeMap<String, String>();
107     if (details.isUse10a()) {
108       additionalParameters.put(OAuthConsumerParameter.oauth_callback.toString(), callback);
109     }
110     Map<String, String> specifiedParams = details.getAdditionalParameters();
111     if (specifiedParams != null) {
112       additionalParameters.putAll(specifiedParams);
113     }
114     return getTokenFromProvider(details, requestTokenURL, httpMethod, null, additionalParameters);
115   }
116 
117   // Inherited.
118   public OAuthConsumerToken/org/springframework/security/oauth/consumer/OAuthConsumerToken.html#OAuthConsumerToken">OAuthConsumerToken getAccessToken(OAuthConsumerToken requestToken, String verifier) throws OAuthRequestFailedException {
119     ProtectedResourceDetails details = getProtectedResourceDetailsService().loadProtectedResourceDetailsById(requestToken.getResourceId());
120     return getAccessToken(details, requestToken, verifier);
121   }
122 
123   public OAuthConsumerTokenh/consumer/OAuthConsumerToken.html#OAuthConsumerToken">OAuthConsumerToken getAccessToken(ProtectedResourceDetails details, OAuthConsumerToken requestToken, String verifier) {
124     URL accessTokenURL;
125     try {
126       accessTokenURL = new URL(details.getAccessTokenURL());
127     }
128     catch (MalformedURLException e) {
129       throw new IllegalStateException("Malformed URL for obtaining an access token.", e);
130     }
131 
132     String httpMethod = details.getAccessTokenHttpMethod();
133 
134     Map<String, String> additionalParameters = new TreeMap<String, String>();
135     if (details.isUse10a()) {
136       if (verifier == null) {
137         throw new UnverifiedRequestTokenException("Unverified request token: " + requestToken);
138       }
139       additionalParameters.put(OAuthConsumerParameter.oauth_verifier.toString(), verifier);
140     }
141     Map<String, String> specifiedParams = details.getAdditionalParameters();
142     if (specifiedParams != null) {
143       additionalParameters.putAll(specifiedParams);
144     }
145     return getTokenFromProvider(details, accessTokenURL, httpMethod, requestToken, additionalParameters);
146   }
147 
148   // Inherited.
149   public InputStream readProtectedResource(URL url, OAuthConsumerToken accessToken, String httpMethod) throws OAuthRequestFailedException {
150     if (accessToken == null) {
151       throw new OAuthRequestFailedException("A valid access token must be supplied.");
152     }
153 
154     ProtectedResourceDetails resourceDetails = getProtectedResourceDetailsService().loadProtectedResourceDetailsById(accessToken.getResourceId());
155     if ((!resourceDetails.isAcceptsAuthorizationHeader()) && !"POST".equalsIgnoreCase(httpMethod) && !"PUT".equalsIgnoreCase(httpMethod)) {
156       throw new IllegalArgumentException("Protected resource " + resourceDetails.getId() + " cannot be accessed with HTTP method " +
157         httpMethod + " because the OAuth provider doesn't accept the OAuth Authorization header.");
158     }
159 
160     return readResource(resourceDetails, url, httpMethod, accessToken, resourceDetails.getAdditionalParameters(), null);
161   }
162 
163   /**
164    * Read a resource.
165    *
166    * @param details The details of the resource.
167    * @param url The URL of the resource.
168    * @param httpMethod The http method.
169    * @param token The token.
170    * @param additionalParameters Any additional request parameters.
171    * @param additionalRequestHeaders Any additional request parameters.
172    * @return The resource.
173    */
174   protected InputStream readResource(ProtectedResourceDetails details, URL url, String httpMethod, OAuthConsumerToken token, Map<String, String> additionalParameters, Map<String, String> additionalRequestHeaders) {
175     url = configureURLForProtectedAccess(url, token, details, httpMethod, additionalParameters);
176     String realm = details.getAuthorizationHeaderRealm();
177     boolean sendOAuthParamsInRequestBody = !details.isAcceptsAuthorizationHeader() && (("POST".equalsIgnoreCase(httpMethod) || "PUT".equalsIgnoreCase(httpMethod)));
178     HttpURLConnection connection = openConnection(url);
179 
180     try {
181       connection.setRequestMethod(httpMethod);
182     }
183     catch (ProtocolException e) {
184       throw new IllegalStateException(e);
185     }
186 
187     Map<String, String> reqHeaders = details.getAdditionalRequestHeaders();
188     if (reqHeaders != null) {
189       for (Map.Entry<String, String> requestHeader : reqHeaders.entrySet()) {
190         connection.setRequestProperty(requestHeader.getKey(), requestHeader.getValue());
191       }
192     }
193 
194     if (additionalRequestHeaders != null) {
195       for (Map.Entry<String, String> requestHeader : additionalRequestHeaders.entrySet()) {
196         connection.setRequestProperty(requestHeader.getKey(), requestHeader.getValue());
197       }
198     }
199 
200     int responseCode;
201     String responseMessage;
202     OutputStream out = null;
203     try {
204       connection.setDoOutput(sendOAuthParamsInRequestBody);
205       connection.connect();
206       if (sendOAuthParamsInRequestBody) {
207         String queryString = getOAuthQueryString(details, token, url, httpMethod, additionalParameters);
208         out = connection.getOutputStream();
209         out.write(queryString.getBytes("UTF-8"));
210         out.flush();
211       }
212       responseCode = connection.getResponseCode();
213       responseMessage = connection.getResponseMessage();
214       if (responseMessage == null) {
215         responseMessage = "Unknown Error";
216       }
217     }
218     catch (IOException e) {
219       throw new OAuthRequestFailedException("OAuth connection failed.", e);
220     }
221     finally {
222       try {
223         if (out != null) {
224           out.close();
225         }
226       }
227       catch (IOException e) {
228         logger.warn("Cannot close open stream: ", e);
229       }
230     }
231 
232     if (responseCode >= 200 && responseCode < 300) {
233       try {
234         return connection.getInputStream();
235       }
236       catch (IOException e) {
237         throw new OAuthRequestFailedException("Unable to get the input stream from a successful response.", e);
238       }
239     }
240     else if (responseCode == 400) {
241       throw new OAuthRequestFailedException("OAuth authentication failed: " + responseMessage);
242     }
243     else if (responseCode == 401) {
244       String authHeaderValue = connection.getHeaderField("WWW-Authenticate");
245       if (authHeaderValue != null) {
246         Map<String, String> headerEntries = StringSplitUtils.splitEachArrayElementAndCreateMap(StringSplitUtils.splitIgnoringQuotes(authHeaderValue, ','), "=", "\"");
247         String requiredRealm = headerEntries.get("realm");
248         if ((requiredRealm != null) && (!requiredRealm.equals(realm))) {
249           throw new InvalidOAuthRealmException(String.format("Invalid OAuth realm. Provider expects \"%s\", when the resource details specify \"%s\".", requiredRealm, realm), requiredRealm);
250         }
251       }
252 
253       throw new OAuthRequestFailedException("OAuth authentication failed: " + responseMessage);
254     }
255     else {
256       throw new OAuthRequestFailedException(String.format("Invalid response code %s (%s).", responseCode, responseMessage));
257     }
258   }
259 
260   /**
261    * Create a configured URL.  If the HTTP method to access the resource is "POST" or "PUT" and the "Authorization"
262    * header isn't supported, then the OAuth parameters will be expected to be sent in the body of the request. Otherwise,
263    * you can assume that the given URL is ready to be used without further work.
264    *
265    * @param url         The base URL.
266    * @param accessToken The access token.
267    * @param httpMethod The HTTP method.
268    * @param additionalParameters Any additional request parameters.
269    * @return The configured URL.
270    */
271   public URL configureURLForProtectedAccess(URL url, OAuthConsumerToken accessToken, String httpMethod, Map<String, String> additionalParameters) throws OAuthRequestFailedException {
272     return configureURLForProtectedAccess(url, accessToken, getProtectedResourceDetailsService().loadProtectedResourceDetailsById(accessToken.getResourceId()), httpMethod, additionalParameters);
273   }
274 
275   /**
276    * Internal use of configuring the URL for protected access, the resource details already having been loaded.
277    *
278    * @param url          The URL.
279    * @param requestToken The request token.
280    * @param details      The details.
281    * @param httpMethod   The http method.
282    * @param additionalParameters Any additional request parameters.
283    * @return The configured URL.
284    */
285   protected URL configureURLForProtectedAccess(URL url, OAuthConsumerToken requestToken, ProtectedResourceDetails details, String httpMethod, Map<String, String> additionalParameters) {
286     String file;
287     if (!"POST".equalsIgnoreCase(httpMethod) && !"PUT".equalsIgnoreCase(httpMethod) && !details.isAcceptsAuthorizationHeader()) {
288       StringBuilder fileb = new StringBuilder(url.getPath());
289       String queryString = getOAuthQueryString(details, requestToken, url, httpMethod, additionalParameters);
290       fileb.append('?').append(queryString);
291       file = fileb.toString();
292     }
293     else {
294       file = url.getFile();
295     }
296 
297     try {
298       if ("http".equalsIgnoreCase(url.getProtocol())) {
299         URLStreamHandler streamHandler = getStreamHandlerFactory().getHttpStreamHandler(details, requestToken, this, httpMethod, additionalParameters);
300         return new URL(url.getProtocol(), url.getHost(), url.getPort(), file, streamHandler);
301       }
302       else if ("https".equalsIgnoreCase(url.getProtocol())) {
303         URLStreamHandler streamHandler = getStreamHandlerFactory().getHttpsStreamHandler(details, requestToken, this, httpMethod, additionalParameters);
304         return new URL(url.getProtocol(), url.getHost(), url.getPort(), file, streamHandler);
305       }
306       else {
307         throw new OAuthRequestFailedException("Unsupported OAuth protocol: " + url.getProtocol());
308       }
309     }
310     catch (MalformedURLException e) {
311       throw new IllegalStateException(e);
312     }
313   }
314 
315   // Inherited.
316   public String getAuthorizationHeader(ProtectedResourceDetails details, OAuthConsumerToken accessToken, URL url, String httpMethod, Map<String, String> additionalParameters) {
317     if (!details.isAcceptsAuthorizationHeader()) {
318       return null;
319     }
320     else {
321       Map<String, Set<CharSequence>> oauthParams = loadOAuthParameters(details, url, accessToken, httpMethod, additionalParameters);
322       String realm = details.getAuthorizationHeaderRealm();
323 
324       StringBuilder builder = new StringBuilder("OAuth ");
325       boolean writeComma = false;
326       if (realm != null) { //realm is optional.
327         builder.append("realm=\"").append(realm).append('"');
328         writeComma = true;
329       }
330 
331       for (Map.Entry<String, Set<CharSequence>> paramValuesEntry : oauthParams.entrySet()) {
332         Set<CharSequence> paramValues = paramValuesEntry.getValue();
333         CharSequence paramValue = findValidHeaderValue(paramValues);
334         if (paramValue != null) {
335           if (writeComma) {
336             builder.append(", ");
337           }
338 
339           builder.append(paramValuesEntry.getKey()).append("=\"").append(oauthEncode(paramValue.toString())).append('"');
340           writeComma = true;
341         }
342       }
343 
344       return builder.toString();
345     }
346   }
347 
348   /**
349    * Finds a valid header value that is valid for the OAuth header.
350    *
351    * @param paramValues The possible values for the oauth header.
352    * @return The selected value, or null if none were found.
353    */
354   protected String findValidHeaderValue(Set<CharSequence> paramValues) {
355     String selectedValue = null;
356     if (paramValues != null && !paramValues.isEmpty()) {
357       CharSequence value = paramValues.iterator().next();
358       if (!(value instanceof QueryParameterValue)) {
359         selectedValue = value.toString();
360       }
361     }
362     return selectedValue;
363   }
364 
365   // Inherited.
366   public String getOAuthQueryString(ProtectedResourceDetails details, OAuthConsumerToken accessToken, URL url, String httpMethod, Map<String, String> additionalParameters) {
367     Map<String, Set<CharSequence>> oauthParams = loadOAuthParameters(details, url, accessToken, httpMethod, additionalParameters);
368 
369     StringBuilder queryString = new StringBuilder();
370     if (details.isAcceptsAuthorizationHeader()) {
371       //if the resource accepts the auth header, remove any parameters that will go in the header (don't pass them redundantly in the query string).
372       for (OAuthConsumerParameter oauthParam : OAuthConsumerParameter.values()) {
373         oauthParams.remove(oauthParam.toString());
374       }
375 
376       if (additionalParameters != null) {
377         for (String additionalParam : additionalParameters.keySet()) {
378           oauthParams.remove(additionalParam);
379         }
380       }
381     }
382 
383     Iterator<String> parametersIt = oauthParams.keySet().iterator();
384     while (parametersIt.hasNext()) {
385       String parameter = parametersIt.next();
386       queryString.append(parameter);
387       Set<CharSequence> values = oauthParams.get(parameter);
388       if (values != null) {
389         Iterator<CharSequence> valuesIt = values.iterator();
390         while (valuesIt.hasNext()) {
391           CharSequence parameterValue = valuesIt.next();
392           if (parameterValue != null) {
393             queryString.append('=').append(urlEncode(parameterValue.toString()));
394           }
395           if (valuesIt.hasNext()) {
396             queryString.append('&').append(parameter);
397           }
398         }
399       }
400       if (parametersIt.hasNext()) {
401         queryString.append('&');
402       }
403     }
404 
405     return queryString.toString();
406   }
407 
408   /**
409    * Get the consumer token with the given parameters and URL. The determination of whether the retrieved token
410    * is an access token depends on whether a request token is provided.
411    *
412    * @param details      The resource details.
413    * @param tokenURL     The token URL.
414    * @param httpMethod   The http method.
415    * @param requestToken The request token, or null if none.
416    * @param additionalParameters The additional request parameter.
417    * @return The token.
418    */
419   protected OAuthConsumerToken getTokenFromProvider(ProtectedResourceDetails details, URL tokenURL, String httpMethod,
420                                                     OAuthConsumerToken requestToken, Map<String, String> additionalParameters) {
421     boolean isAccessToken = requestToken != null;
422     if (!isAccessToken) {
423       //create an empty token to make a request for a new unauthorized request token.
424       requestToken = new OAuthConsumerToken();
425     }
426 
427     TreeMap<String, String> requestHeaders = new TreeMap<String, String>();
428     if ("POST".equalsIgnoreCase(httpMethod)) {
429       requestHeaders.put("Content-Type", "application/x-www-form-urlencoded");
430     }
431     InputStream inputStream = readResource(details, tokenURL, httpMethod, requestToken, additionalParameters, requestHeaders);
432     String tokenInfo;
433     try {
434       ByteArrayOutputStream out = new ByteArrayOutputStream();
435       byte[] buffer = new byte[1024];
436       int len = inputStream.read(buffer);
437       while (len >= 0) {
438         out.write(buffer, 0, len);
439         len = inputStream.read(buffer);
440       }
441 
442       tokenInfo = new String(out.toByteArray(), "UTF-8");
443     }
444     catch (IOException e) {
445       throw new OAuthRequestFailedException("Unable to read the token.", e);
446     }
447     finally {
448       try {
449         if (inputStream != null) {
450           inputStream.close();
451         }
452       } 
453       catch (IOException e) {
454         logger.warn("Cannot close open stream: ", e);
455       }
456     }
457 
458     StringTokenizer tokenProperties = new StringTokenizer(tokenInfo, "&");
459     Map<String, String> tokenPropertyValues = new TreeMap<String, String>();
460     while (tokenProperties.hasMoreElements()) {
461       try {
462         String tokenProperty = (String) tokenProperties.nextElement();
463         int equalsIndex = tokenProperty.indexOf('=');
464         if (equalsIndex > 0) {
465           String propertyName = OAuthCodec.oauthDecode(tokenProperty.substring(0, equalsIndex));
466           String propertyValue = OAuthCodec.oauthDecode(tokenProperty.substring(equalsIndex + 1));
467           tokenPropertyValues.put(propertyName, propertyValue);
468         }
469         else {
470           tokenProperty = OAuthCodec.oauthDecode(tokenProperty);
471           tokenPropertyValues.put(tokenProperty, null);
472         }
473       }
474       catch (DecoderException e) {
475         throw new OAuthRequestFailedException("Unable to decode token parameters.");
476       }
477     }
478 
479     String tokenValue = tokenPropertyValues.remove(OAuthProviderParameter.oauth_token.toString());
480     if (tokenValue == null) {
481       throw new OAuthRequestFailedException("OAuth provider failed to return a token.");
482     }
483 
484     String tokenSecret = tokenPropertyValues.remove(OAuthProviderParameter.oauth_token_secret.toString());
485     if (tokenSecret == null) {
486       throw new OAuthRequestFailedException("OAuth provider failed to return a token secret.");
487     }
488 
489     OAuthConsumerTokener/OAuthConsumerToken.html#OAuthConsumerToken">OAuthConsumerToken consumerToken = new OAuthConsumerToken();
490     consumerToken.setValue(tokenValue);
491     consumerToken.setSecret(tokenSecret);
492     consumerToken.setResourceId(details.getId());
493     consumerToken.setAccessToken(isAccessToken);
494     if (!tokenPropertyValues.isEmpty()) {
495       consumerToken.setAdditionalParameters(tokenPropertyValues);
496     }
497     return consumerToken;
498   }
499 
500   /**
501    * Loads the OAuth parameters for the given resource at the given URL and the given token. These parameters include
502    * any query parameters on the URL since they are included in the signature. The oauth parameters are NOT encoded.
503    *
504    * @param details      The resource details.
505    * @param requestURL   The request URL.
506    * @param requestToken The request token.
507    * @param httpMethod   The http method.
508    * @param additionalParameters Additional oauth parameters (outside of the core oauth spec).
509    * @return The parameters.
510    */
511   protected Map<String, Set<CharSequence>> loadOAuthParameters(ProtectedResourceDetails details, URL requestURL, OAuthConsumerToken requestToken, String httpMethod, Map<String, String> additionalParameters) {
512     Map<String, Set<CharSequence>> oauthParams = new TreeMap<String, Set<CharSequence>>();
513 
514     if (additionalParameters != null) {
515       for (Map.Entry<String, String> additionalParam : additionalParameters.entrySet()) {
516         Set<CharSequence> values = oauthParams.get(additionalParam.getKey());
517         if (values == null) {
518           values = new HashSet<CharSequence>();
519           oauthParams.put(additionalParam.getKey(), values);
520         }
521         if (additionalParam.getValue() != null) {
522           values.add(additionalParam.getValue());
523         }
524       }
525     }
526     
527     String query = requestURL.getQuery();
528     if (query != null) {
529       StringTokenizer queryTokenizer = new StringTokenizer(query, "&");
530       while (queryTokenizer.hasMoreElements()) {
531         String token = (String) queryTokenizer.nextElement();
532         CharSequence value = null;
533         int equalsIndex = token.indexOf('=');
534         if (equalsIndex < 0) {
535           token = urlDecode(token);
536         }
537         else {
538           value = new QueryParameterValue(urlDecode(token.substring(equalsIndex + 1)));
539           token = urlDecode(token.substring(0, equalsIndex));
540         }
541 
542         Set<CharSequence> values = oauthParams.get(token);
543         if (values == null) {
544           values = new HashSet<CharSequence>();
545           oauthParams.put(token, values);
546         }
547         if (value != null) {
548           values.add(value);
549         }
550       }
551     }
552 
553     String tokenSecret = requestToken == null ? null : requestToken.getSecret();
554     String nonce = getNonceFactory().generateNonce();
555     oauthParams.put(OAuthConsumerParameter.oauth_consumer_key.toString(), Collections.singleton((CharSequence) details.getConsumerKey()));
556     if ((requestToken != null) && (requestToken.getValue() != null)) {
557       oauthParams.put(OAuthConsumerParameter.oauth_token.toString(), Collections.singleton((CharSequence) requestToken.getValue()));
558     }
559 
560     oauthParams.put(OAuthConsumerParameter.oauth_nonce.toString(), Collections.singleton((CharSequence) nonce));
561     oauthParams.put(OAuthConsumerParameter.oauth_signature_method.toString(), Collections.singleton((CharSequence) details.getSignatureMethod()));
562     oauthParams.put(OAuthConsumerParameter.oauth_timestamp.toString(), Collections.singleton((CharSequence) String.valueOf(System.currentTimeMillis() / 1000)));
563     oauthParams.put(OAuthConsumerParameter.oauth_version.toString(), Collections.singleton((CharSequence) "1.0"));
564     String signatureBaseString = getSignatureBaseString(oauthParams, requestURL, httpMethod);
565     OAuthSignatureMethod signatureMethod;
566     try {
567       signatureMethod = getSignatureFactory().getSignatureMethod(details.getSignatureMethod(), details.getSharedSecret(), tokenSecret);
568     }
569     catch (UnsupportedSignatureMethodException e) {
570       throw new OAuthRequestFailedException(e.getMessage(), e);
571     }
572     String signature = signatureMethod.sign(signatureBaseString);
573     oauthParams.put(OAuthConsumerParameter.oauth_signature.toString(), Collections.singleton((CharSequence) signature));
574     return oauthParams;
575   }
576 
577   /**
578    * URL-encode a value.
579    *
580    * @param value The value to encode.
581    * @return The URL-encoded value.
582    */
583   protected String urlEncode(String value) {
584     try {
585       return URLEncoder.encode(value, "UTF-8");
586     }
587     catch (UnsupportedEncodingException e) {
588       throw new RuntimeException(e);
589     }
590   }
591 
592   /**
593    * URL-decode a token.
594    *
595    * @param token The token to URL-decode.
596    * @return The decoded token.
597    */
598   protected String urlDecode(String token) {
599     try {
600       return URLDecoder.decode(token, "utf-8");
601     }
602     catch (UnsupportedEncodingException e) {
603       throw new RuntimeException(e);
604     }
605   }
606 
607   /**
608    * Open a connection to the given URL.
609    *
610    * @param requestTokenURL The request token URL.
611    * @return The HTTP URL connection.
612    */
613   protected HttpURLConnection openConnection(URL requestTokenURL) {
614     try {
615       HttpURLConnection connection = (HttpURLConnection) requestTokenURL.openConnection(selectProxy(requestTokenURL));
616       connection.setConnectTimeout(getConnectionTimeout());
617       connection.setReadTimeout(getReadTimeout());
618       return connection;
619     }
620     catch (IOException e) {
621       throw new OAuthRequestFailedException("Failed to open an OAuth connection.", e);
622     }
623   }
624 
625   /**
626    * Selects a proxy for the given URL.
627    *
628    * @param requestTokenURL The URL
629    * @return The proxy.
630    */
631   protected Proxy selectProxy(URL requestTokenURL) {
632     try {
633       List<Proxy> selectedProxies = getProxySelector().select(requestTokenURL.toURI());
634       return selectedProxies.isEmpty() ? Proxy.NO_PROXY : selectedProxies.get(0);
635     }
636     catch (URISyntaxException e) {
637       throw new IllegalArgumentException(e);
638     }
639   }
640 
641   /**
642    * Get the signature base string for the specified parameters. It is presumed the parameters are NOT OAuth-encoded.
643    *
644    * @param oauthParams The parameters (NOT oauth-encoded).
645    * @param requestURL  The request URL.
646    * @param httpMethod  The http method.
647    * @return The signature base string.
648    */
649   protected String getSignatureBaseString(Map<String, Set<CharSequence>> oauthParams, URL requestURL, String httpMethod) {
650     TreeMap<String, TreeSet<String>> sortedParameters = new TreeMap<String, TreeSet<String>>();
651 
652     for (Map.Entry<String, Set<CharSequence>> param : oauthParams.entrySet()) {
653       //first encode all parameter names and values (spec section 9.1)
654       String key = oauthEncode(param.getKey());
655 
656       //add the encoded parameters sorted according to the spec.
657       TreeSet<String> sortedValues = sortedParameters.get(key);
658       if (sortedValues == null) {
659         sortedValues = new TreeSet<String>();
660         sortedParameters.put(key, sortedValues);
661       }
662 
663       for (CharSequence value : param.getValue()) {
664         sortedValues.add(oauthEncode(value.toString()));
665       }
666     }
667 
668     //now concatenate them into a single query string according to the spec.
669     StringBuilder queryString = new StringBuilder();
670     Iterator<Map.Entry<String, TreeSet<String>>> sortedIt = sortedParameters.entrySet().iterator();
671     while (sortedIt.hasNext()) {
672       Map.Entry<String, TreeSet<String>> sortedParameter = sortedIt.next();
673       for (Iterator<String> sortedParametersIterator = sortedParameter.getValue().iterator(); sortedParametersIterator.hasNext();) {
674 		String parameterValue = sortedParametersIterator.next();
675 		if (parameterValue == null) {
676           parameterValue = "";
677         }
678 
679         queryString.append(sortedParameter.getKey()).append('=').append(parameterValue);
680         if (sortedIt.hasNext() || sortedParametersIterator.hasNext()) {
681           queryString.append('&');
682         }
683 	}
684     }
685 
686     StringBuilder url = new StringBuilder(requestURL.getProtocol().toLowerCase()).append("://").append(requestURL.getHost().toLowerCase());
687     if ((requestURL.getPort() >= 0) && (requestURL.getPort() != requestURL.getDefaultPort())) {
688       url.append(":").append(requestURL.getPort());
689     }
690     url.append(requestURL.getPath());
691     
692     return new StringBuilder(httpMethod.toUpperCase()).append('&').append(oauthEncode(url.toString())).append('&').append(oauthEncode(queryString.toString())).toString();
693   }
694 
695   /**
696    * The protected resource details service.
697    *
698    * @return The protected resource details service.
699    */
700   public ProtectedResourceDetailsService getProtectedResourceDetailsService() {
701     return protectedResourceDetailsService;
702   }
703 
704   /**
705    * The protected resource details service.
706    *
707    * @param protectedResourceDetailsService
708    *         The protected resource details service.
709    */
710   @Autowired
711   public void setProtectedResourceDetailsService(ProtectedResourceDetailsService protectedResourceDetailsService) {
712     this.protectedResourceDetailsService = protectedResourceDetailsService;
713   }
714 
715   /**
716    * The URL stream handler factory for connections to an OAuth resource.
717    *
718    * @return The URL stream handler factory for connections to an OAuth resource.
719    */
720   public OAuthURLStreamHandlerFactory getStreamHandlerFactory() {
721     return streamHandlerFactory;
722   }
723 
724   /**
725    * The URL stream handler factory for connections to an OAuth resource.
726    *
727    * @param streamHandlerFactory The URL stream handler factory for connections to an OAuth resource.
728    */
729   @Autowired (required = false)
730   public void setStreamHandlerFactory(OAuthURLStreamHandlerFactory streamHandlerFactory) {
731     this.streamHandlerFactory = streamHandlerFactory;
732   }
733 
734   /**
735    * The nonce factory.
736    *
737    * @return The nonce factory.
738    */
739   public NonceFactory getNonceFactory() {
740     return nonceFactory;
741   }
742 
743   /**
744    * The nonce factory.
745    *
746    * @param nonceFactory The nonce factory.
747    */
748   @Autowired (required = false)
749   public void setNonceFactory(NonceFactory nonceFactory) {
750     this.nonceFactory = nonceFactory;
751   }
752 
753   /**
754    * The signature factory to use.
755    *
756    * @return The signature factory to use.
757    */
758   public OAuthSignatureMethodFactory getSignatureFactory() {
759     return signatureFactory;
760   }
761 
762   /**
763    * The signature factory to use.
764    *
765    * @param signatureFactory The signature factory to use.
766    */
767   @Autowired (required = false)
768   public void setSignatureFactory(OAuthSignatureMethodFactory signatureFactory) {
769     this.signatureFactory = signatureFactory;
770   }
771 
772   /**
773    * The proxy selector to use.
774    *
775    * @return The proxy selector to use.
776    */
777   public ProxySelector getProxySelector() {
778     return proxySelector;
779   }
780 
781   /**
782    * The proxy selector to use.
783    *
784    * @param proxySelector The proxy selector to use.
785    */
786   @Autowired (required = false)
787   public void setProxySelector(ProxySelector proxySelector) {
788     this.proxySelector = proxySelector;
789   }
790 
791   /**
792    * The connection timeout (default 60 seconds).
793    *
794    * @return The connection timeout.
795    */
796   public int getConnectionTimeout() {
797     return connectionTimeout;
798   }
799 
800   /**
801    * The connection timeout.
802    *
803    * @param connectionTimeout The connection timeout.
804    */
805   public void setConnectionTimeout(int connectionTimeout) {
806     this.connectionTimeout = connectionTimeout;
807   }
808 
809   /**
810    * The read timeout (default 60 seconds).
811    *
812    * @return The read timeout.
813    */
814   public int getReadTimeout() {
815     return readTimeout;
816   }
817 
818   /**
819    * The read timeout.
820    *
821    * @param readTimeout The read timeout.
822    */
823   public void setReadTimeout(int readTimeout) {
824     this.readTimeout = readTimeout;
825   }
826 
827   /**
828    * Marker class for an oauth parameter value that is a query parameter and should therefore not be included in the authorization header.
829    */
830   public static class QueryParameterValue implements CharSequence {
831 
832     private final String value;
833 
834     public QueryParameterValue(String value) {
835       this.value = value;
836     }
837 
838     public int length() {
839       return this.value.length();
840     }
841 
842     public char charAt(int index) {
843       return this.value.charAt(index);
844     }
845 
846     public CharSequence subSequence(int start, int end) {
847       return this.value.subSequence(start, end);
848     }
849 
850     @Override
851     public String toString() {
852       return this.value;
853     }
854 
855     @Override
856     public int hashCode() {
857       return this.value.hashCode();
858     }
859 
860     @Override
861     public boolean equals(Object obj) {
862       return this.value.equals(obj);
863     }
864   }
865 }