/* * Copyright (c) 2009 Matthias Kaeppler 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 oauth.signpost; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthExpectationFailedException; import oauth.signpost.exception.OAuthMessageSignerException; import oauth.signpost.exception.OAuthNotAuthorizedException; import oauth.signpost.http.HttpParameters; import oauth.signpost.http.HttpRequest; import oauth.signpost.http.HttpResponse; /** * ABC for all provider implementations. If you're writing a custom provider, * you will probably inherit from this class, since it takes a lot of work from * you. * * @author Matthias Kaeppler */ public abstract class AbstractOAuthProvider implements OAuthProvider { private static final long serialVersionUID = 1L; private String requestTokenEndpointUrl; private String accessTokenEndpointUrl; private String authorizationWebsiteUrl; private HttpParameters responseParameters; private Map<String, String> defaultHeaders; private boolean isOAuth10a; private transient OAuthProviderListener listener; public AbstractOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl, String authorizationWebsiteUrl) { this.requestTokenEndpointUrl = requestTokenEndpointUrl; this.accessTokenEndpointUrl = accessTokenEndpointUrl; this.authorizationWebsiteUrl = authorizationWebsiteUrl; this.responseParameters = new HttpParameters(); this.defaultHeaders = new HashMap<String, String>(); } @Override public synchronized String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl, String... customOAuthParams) throws OAuthMessageSignerException, OAuthNotAuthorizedException, OAuthExpectationFailedException, OAuthCommunicationException { // invalidate current credentials, if any consumer.setTokenWithSecret(null, null); // 1.0a expects the callback to be sent while getting the request token. // 1.0 service providers would simply ignore this parameter. HttpParameters params = new HttpParameters(); params.putAll(customOAuthParams, true); params.put(OAuth.OAUTH_CALLBACK, callbackUrl, true); retrieveToken(consumer, requestTokenEndpointUrl, params); String callbackConfirmed = responseParameters.getFirst(OAuth.OAUTH_CALLBACK_CONFIRMED); responseParameters.remove(OAuth.OAUTH_CALLBACK_CONFIRMED); isOAuth10a = Boolean.TRUE.toString().equals(callbackConfirmed); // 1.0 service providers expect the callback as part of the auth URL, // Do not send when 1.0a. if (isOAuth10a) { return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN, consumer.getToken()); } else { return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN, consumer.getToken(), OAuth.OAUTH_CALLBACK, callbackUrl); } } @Override public synchronized void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier, String... customOAuthParams) throws OAuthMessageSignerException, OAuthNotAuthorizedException, OAuthExpectationFailedException, OAuthCommunicationException { if (consumer.getToken() == null || consumer.getTokenSecret() == null) { throw new OAuthExpectationFailedException( "Authorized request token or token secret not set. " + "Did you retrieve an authorized request token before?"); } HttpParameters params = new HttpParameters(); params.putAll(customOAuthParams, true); if (isOAuth10a && oauthVerifier != null) { params.put(OAuth.OAUTH_VERIFIER, oauthVerifier, true); } retrieveToken(consumer, accessTokenEndpointUrl, params); } /** * <p> * Implemented by subclasses. The responsibility of this method is to * contact the service provider at the given endpoint URL and fetch a * request or access token. What kind of token is retrieved solely depends * on the URL being used. * </p> * <p> * Correct implementations of this method must guarantee the following * post-conditions: * <ul> * <li>the {@link OAuthConsumer} passed to this method must have a valid * {@link OAuth#OAUTH_TOKEN} and {@link OAuth#OAUTH_TOKEN_SECRET} set by * calling {@link OAuthConsumer#setTokenWithSecret(String, String)}</li> * <li>{@link #getResponseParameters()} must return the set of query * parameters served by the service provider in the token response, with all * OAuth specific parameters being removed</li> * </ul> * </p> * * @param consumer * the {@link OAuthConsumer} that should be used to sign the request * @param endpointUrl * the URL at which the service provider serves the OAuth token that * is to be fetched * @param customOAuthParams * you can pass custom OAuth parameters here (such as oauth_callback * or oauth_verifier) which will go directly into the signer, i.e. * you don't have to put them into the request first. * @throws OAuthMessageSignerException * if signing the token request fails * @throws OAuthCommunicationException * if a network communication error occurs * @throws OAuthNotAuthorizedException * if the server replies 401 - Unauthorized * @throws OAuthExpectationFailedException * if an expectation has failed, e.g. because the server didn't * reply in the expected format */ protected void retrieveToken(OAuthConsumer consumer, String endpointUrl, HttpParameters customOAuthParams) throws OAuthMessageSignerException, OAuthCommunicationException, OAuthNotAuthorizedException, OAuthExpectationFailedException { Map<String, String> defaultHeaders = getRequestHeaders(); if (consumer.getConsumerKey() == null || consumer.getConsumerSecret() == null) { throw new OAuthExpectationFailedException("Consumer key or secret not set"); } HttpRequest request = null; HttpResponse response = null; try { request = createRequest(endpointUrl); for (String header : defaultHeaders.keySet()) { request.setHeader(header, defaultHeaders.get(header)); } if (customOAuthParams != null && !customOAuthParams.isEmpty()) { consumer.setAdditionalParameters(customOAuthParams); } if (this.listener != null) { this.listener.prepareRequest(request); } consumer.sign(request); if (this.listener != null) { this.listener.prepareSubmission(request); } response = sendRequest(request); int statusCode = response.getStatusCode(); boolean requestHandled = false; if (this.listener != null) { requestHandled = this.listener.onResponseReceived(request, response); } if (requestHandled) { return; } if (statusCode >= 300) { handleUnexpectedResponse(statusCode, response); } HttpParameters responseParams = OAuth.decodeForm(response.getContent()); String token = responseParams.getFirst(OAuth.OAUTH_TOKEN); String secret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET); responseParams.remove(OAuth.OAUTH_TOKEN); responseParams.remove(OAuth.OAUTH_TOKEN_SECRET); setResponseParameters(responseParams); if (token == null || secret == null) { throw new OAuthExpectationFailedException( "Request token or token secret not set in server reply. " + "The service provider you use is probably buggy."); } consumer.setTokenWithSecret(token, secret); } catch (OAuthNotAuthorizedException e) { throw e; } catch (OAuthExpectationFailedException e) { throw e; } catch (Exception e) { throw new OAuthCommunicationException(e); } finally { try { closeConnection(request, response); } catch (Exception e) { throw new OAuthCommunicationException(e); } } } protected void handleUnexpectedResponse(int statusCode, HttpResponse response) throws Exception { if (response == null) { return; } StringBuilder responseBody = new StringBuilder(); InputStream content = response.getContent(); if (content != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(content)); String line = reader.readLine(); while (line != null) { responseBody.append(line); line = reader.readLine(); } } switch (statusCode) { case 401: throw new OAuthNotAuthorizedException(responseBody.toString()); default: throw new OAuthCommunicationException("Service provider responded in error: " + statusCode + " (" + response.getReasonPhrase() + ")", responseBody.toString()); } } /** * Overrride this method if you want to customize the logic for building a * request object for the given endpoint URL. * * @param endpointUrl * the URL to which the request will go * @return the request object * @throws Exception * if something breaks */ protected abstract HttpRequest createRequest(String endpointUrl) throws Exception; /** * Override this method if you want to customize the logic for how the given * request is sent to the server. * * @param request * the request to send * @return the response to the request * @throws Exception * if something breaks */ protected abstract HttpResponse sendRequest(HttpRequest request) throws Exception; /** * Called when the connection is being finalized after receiving the * response. Use this to do any cleanup / resource freeing. * * @param request * the request that has been sent * @param response * the response that has been received * @throws Exception * if something breaks */ protected void closeConnection(HttpRequest request, HttpResponse response) throws Exception { // NOP } @Override public HttpParameters getResponseParameters() { return responseParameters; } /** * Returns a single query parameter as served by the service provider in a * token reply. You must call {@link #setResponseParameters} with the set of * parameters before using this method. * * @param key * the parameter name * @return the parameter value */ protected String getResponseParameter(String key) { return responseParameters.getFirst(key); } @Override public void setResponseParameters(HttpParameters parameters) { this.responseParameters = parameters; } @Override public void setOAuth10a(boolean isOAuth10aProvider) { this.isOAuth10a = isOAuth10aProvider; } @Override public boolean isOAuth10a() { return isOAuth10a; } @Override public String getRequestTokenEndpointUrl() { return this.requestTokenEndpointUrl; } @Override public String getAccessTokenEndpointUrl() { return this.accessTokenEndpointUrl; } @Override public String getAuthorizationWebsiteUrl() { return this.authorizationWebsiteUrl; } @Override public void setRequestHeader(String header, String value) { defaultHeaders.put(header, value); } @Override public Map<String, String> getRequestHeaders() { return defaultHeaders; } @Override public void setListener(OAuthProviderListener listener) { this.listener = listener; } @Override public void removeListener(OAuthProviderListener listener) { this.listener = null; } }