/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.social.oauth1;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.social.support.ClientHttpRequestFactorySelector;
import org.springframework.social.support.LoggingErrorHandler;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
/**
* OAuth10Operations implementation that uses REST-template to make the OAuth calls.
* @author Keith Donald
*/
public class OAuth1Template implements OAuth1Operations {
private final String consumerKey;
private final String consumerSecret;
private final URI requestTokenUrl;
private final String authenticateUrl;
private final String authorizeUrl;
private final URI accessTokenUrl;
private final RestTemplate restTemplate;
private final OAuth1Version version;
private final SigningSupport signingUtils;
public OAuth1Template(String consumerKey, String consumerSecret, String requestTokenUrl, String authorizeUrl, String accessTokenUrl) {
this(consumerKey, consumerSecret, requestTokenUrl, authorizeUrl, accessTokenUrl, OAuth1Version.CORE_10_REVISION_A);
}
public OAuth1Template(String consumerKey, String consumerSecret, String requestTokenUrl, String authorizeUrl, String accessTokenUrl, OAuth1Version version) {
this(consumerKey, consumerSecret, requestTokenUrl, authorizeUrl, null, accessTokenUrl, version);
}
public OAuth1Template(String consumerKey, String consumerSecret, String requestTokenUrl, String authorizeUrl, String authenticateUrl, String accessTokenUrl) {
this(consumerKey, consumerSecret, requestTokenUrl, authorizeUrl, authenticateUrl, accessTokenUrl, OAuth1Version.CORE_10_REVISION_A);
}
public OAuth1Template(String consumerKey, String consumerSecret, String requestTokenUrl, String authorizeUrl, String authenticateUrl, String accessTokenUrl, OAuth1Version version) {
Assert.notNull(consumerKey, "The consumerKey property cannot be null");
Assert.notNull(consumerSecret, "The consumerSecret property cannot be null");
Assert.notNull(requestTokenUrl, "The requestTokenUrl property cannot be null");
Assert.notNull(authorizeUrl, "The authorizeUrl property cannot be null");
Assert.notNull(accessTokenUrl, "The accessTokenUrl property cannot be null");
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.requestTokenUrl = encodeTokenUri(requestTokenUrl);
this.authorizeUrl = authorizeUrl;
this.authenticateUrl = authenticateUrl;
this.accessTokenUrl = encodeTokenUri(accessTokenUrl);
this.version = version;
this.restTemplate = createRestTemplate();
this.signingUtils = new SigningSupport();
}
/**
* Set the request factory on the underlying RestTemplate.
* This can be used to plug in a different HttpClient to do things like configure custom SSL settings.
* @param requestFactory the request factory on the underlying RestTemplate.
*/
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
Assert.notNull(requestFactory, "The requestFactory property cannot be null");
restTemplate.setRequestFactory(requestFactory);
}
// implementing OAuth1Operations
public OAuth1Version getVersion() {
return version;
}
public OAuthToken fetchRequestToken(String callbackUrl, MultiValueMap<String, String> additionalParameters) {
Map<String, String> oauthParameters = new HashMap<String, String>(1, 1);
if (version == OAuth1Version.CORE_10_REVISION_A) {
oauthParameters.put("oauth_callback", callbackUrl);
}
return exchangeForToken(requestTokenUrl, oauthParameters, additionalParameters, null);
}
public String buildAuthorizeUrl(String requestToken, OAuth1Parameters parameters) {
return buildAuthUrl(authorizeUrl, requestToken, parameters);
}
public String buildAuthenticateUrl(String requestToken, OAuth1Parameters parameters) {
return authenticateUrl != null ? buildAuthUrl(authenticateUrl, requestToken, parameters) : buildAuthorizeUrl(requestToken, parameters);
}
public OAuthToken exchangeForAccessToken(AuthorizedRequestToken requestToken, MultiValueMap<String, String> additionalParameters) {
Map<String, String> tokenParameters = new HashMap<String, String>(2, 1);
tokenParameters.put("oauth_token", requestToken.getValue());
if (version == OAuth1Version.CORE_10_REVISION_A) {
tokenParameters.put("oauth_verifier", requestToken.getVerifier());
}
return exchangeForToken(accessTokenUrl, tokenParameters, additionalParameters, requestToken.getSecret());
}
// subclassing hooks
/**
* Exposes the consumer key to be read by subclasses.
* This may be useful when overriding {@link #addCustomAuthorizationParameters(MultiValueMap)} and the consumer key is required in the authorization request.
* @return the consumer key to be read by subclasses.
*/
protected String getConsumerKey() {
return consumerKey;
}
/**
* Creates an {@link OAuthToken} given the response from the request token or access token exchange with the provider.
* May be overridden to create a custom {@link OAuthToken}.
* @param tokenValue the token value received from the provider.
* @param tokenSecret the token secret received from the provider.
* @param response all parameters from the response received in the request/access token exchange.
* @return an {@link OAuthToken}
*/
protected OAuthToken createOAuthToken(String tokenValue, String tokenSecret, MultiValueMap<String, String> response) {
return new OAuthToken(tokenValue, tokenSecret);
}
/**
* Subclassing hook to add custom authorization parameters to the authorization URL.
* Default implementation adds no parameters.
* @param parameters custom parameters for authorization
*/
protected void addCustomAuthorizationParameters(MultiValueMap<String, String> parameters) {
}
// internal helpers
private RestTemplate createRestTemplate() {
RestTemplate restTemplate = new RestTemplate(ClientHttpRequestFactorySelector.getRequestFactory());
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(1);
converters.add(new FormHttpMessageConverter() {
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// always read MultiValueMaps as x-www-url-formencoded even if contentType not set properly by provider
return MultiValueMap.class.isAssignableFrom(clazz);
}
});
restTemplate.setMessageConverters(converters);
restTemplate.setErrorHandler(new LoggingErrorHandler());
return restTemplate;
}
private URI encodeTokenUri(String url) {
return UriComponentsBuilder.fromUriString(url).build().toUri();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private OAuthToken exchangeForToken(URI tokenUrl, Map<String, String> tokenParameters, MultiValueMap<String, String> additionalParameters, String tokenSecret) {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", buildAuthorizationHeaderValue(tokenUrl, tokenParameters, additionalParameters, tokenSecret));
ResponseEntity<MultiValueMap> response = restTemplate.exchange(tokenUrl, HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(additionalParameters, headers), MultiValueMap.class);
MultiValueMap<String, String> body = response.getBody();
return createOAuthToken(body.getFirst("oauth_token"), body.getFirst("oauth_token_secret"), body);
}
private String buildAuthorizationHeaderValue(URI tokenUrl, Map<String, String> tokenParameters, MultiValueMap<String, String> additionalParameters, String tokenSecret) {
Map<String, String> oauthParameters = signingUtils.commonOAuthParameters(consumerKey);
oauthParameters.putAll(tokenParameters);
if (additionalParameters == null) {
additionalParameters = EmptyMultiValueMap.instance();
}
return signingUtils.buildAuthorizationHeaderValue(HttpMethod.POST, tokenUrl, oauthParameters, additionalParameters, consumerSecret, tokenSecret);
}
private String buildAuthUrl(String baseAuthUrl, String requestToken, OAuth1Parameters parameters) {
StringBuilder authUrl = new StringBuilder(baseAuthUrl).append('?').append("oauth_token").append('=').append(formEncode(requestToken));
addCustomAuthorizationParameters(parameters);
if (parameters != null) {
for (Iterator<Entry<String, List<String>>> additionalParams = parameters.entrySet().iterator(); additionalParams.hasNext();) {
Entry<String, List<String>> param = additionalParams.next();
String name = formEncode(param.getKey());
for (Iterator<String> values = param.getValue().iterator(); values.hasNext();) {
authUrl.append('&').append(name).append('=').append(formEncode(values.next()));
}
}
}
return authUrl.toString();
}
private String formEncode(String data) {
try {
return URLEncoder.encode(data, "UTF-8");
}
catch (UnsupportedEncodingException ex) {
// should not happen, UTF-8 is always supported
throw new IllegalStateException(ex);
}
}
// testing hooks
RestTemplate getRestTemplate() {
return restTemplate;
}
}