org.apache.shindig.gadgets.oauth.OAuthRequest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of shindig-gadgets Show documentation
Show all versions of shindig-gadgets Show documentation
Renders gadgets, provides the gadget metadata service, and serves
all javascript required by the OpenSocial specification.
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to you 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.apache.shindig.gadgets.oauth;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
import net.oauth.OAuth.Parameter;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shindig.auth.OAuthConstants;
import org.apache.shindig.auth.OAuthUtil;
import org.apache.shindig.common.crypto.Crypto;
import org.apache.shindig.common.logging.i18n.MessageKeys;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.uri.UriBuilder;
import org.apache.shindig.common.util.CharsetUtil;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.http.HttpFetcher;
import org.apache.shindig.gadgets.http.HttpRequest;
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.http.HttpResponseBuilder;
import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
/**
* Implements both signed fetch and full OAuth for gadgets, as well as a combination of the two that
* is necessary to build OAuth enabled gadgets for social sites.
*
* Signed fetch sticks identity information in the query string, signed either with the container's
* private key, or else with a secret shared between the container and the gadget.
*
* Full OAuth redirects the user to the OAuth service provider site to obtain the user's permission
* to access their data. Read the example in the appendix to the OAuth spec for a summary of how
* this works (The spec is at http://oauth.net/core/1.0/).
*
* The combination protocol works by sending identity information in all requests, and allows the
* OAuth dance to happen as well when owner == viewer (by default) or for any viewer when the
* OAuthFetcherConfig#isViewerAccessTokensEnabled parameter is true. This lets OAuth service providers build up
* an identity mapping from ids on social network sites to their own local ids.
*/
public class OAuthRequest {
//class name for logging purpose
private static final String classname = OAuthRequest.class.getName();
// Maximum number of attempts at the protocol before giving up.
private static final int MAX_ATTEMPTS = 2;
// names of additional OAuth parameters we include in outgoing requests
// TODO(beaton): can we do away with this bit in favor of the opensocial param?
public static final String XOAUTH_APP_URL = "xoauth_app_url";
protected static final String OPENSOCIAL_OWNERID = "opensocial_owner_id";
protected static final String OPENSOCIAL_VIEWERID = "opensocial_viewer_id";
protected static final String OPENSOCIAL_APPID = "opensocial_app_id";
// TODO(beaton): figure out if this is the name in the 0.8 spec.
protected static final String OPENSOCIAL_APPURL = "opensocial_app_url";
protected static final String OPENSOCIAL_PROXIED_CONTENT = "opensocial_proxied_content";
// old and new parameters for the public key
// TODO remove OLD in a far future release
protected static final String XOAUTH_PUBLIC_KEY_OLD = "xoauth_signature_publickey";
protected static final String XOAUTH_PUBLIC_KEY_NEW = "xoauth_public_key";
protected static final Pattern ALLOWED_PARAM_NAME = Pattern.compile("[-:\\w~!@$*()_\\[\\]:,./ ]+");
private static final long ACCESS_TOKEN_EXPIRE_UNKNOWN = 0;
private static final long ACCESS_TOKEN_FORCE_EXPIRE = -1;
/**
* Configuration options for the fetcher.
*/
protected final OAuthFetcherConfig fetcherConfig;
/**
* Next fetcher to use in chain.
*/
private final HttpFetcher fetcher;
/**
* Additional trusted parameters to be included in the OAuth request.
*/
private final List trustedParams;
/**
* State information from client
*/
protected OAuthClientState clientState;
/**
* OAuth specific stuff to include in the response.
*/
protected OAuthResponseParams responseParams;
/**
* The accessor we use for signing messages. This also holds metadata about
* the service provider, such as their URLs and the keys we use to access
* those URLs.
*/
protected AccessorInfo accessorInfo;
/**
* The request the client really wants to make.
*/
protected HttpRequest realRequest;
/**
* Data returned along with OAuth access token, null if this is not an access token request
*/
protected Map accessTokenData;
/**
* @param fetcherConfig configuration options for the fetcher
* @param fetcher fetcher to use for actually making requests
*/
public OAuthRequest(OAuthFetcherConfig fetcherConfig, HttpFetcher fetcher) {
this(fetcherConfig, fetcher, null);
}
/**
* @param fetcherConfig configuration options for the fetcher
* @param fetcher fetcher to use for actually making requests
* @param trustedParams additional parameters to include in all outgoing OAuth requests, useful
* for client data that can't be pulled from the security token but is still trustworthy.
*/
public OAuthRequest(OAuthFetcherConfig fetcherConfig, HttpFetcher fetcher,
List trustedParams) {
this.fetcherConfig = fetcherConfig;
this.fetcher = fetcher;
this.trustedParams = trustedParams;
}
/**
* OAuth authenticated fetch.
*/
public HttpResponse fetch(HttpRequest request) {
realRequest = request;
clientState = new OAuthClientState(
fetcherConfig.getStateCrypter(),
request.getOAuthArguments().getOrigClientState());
responseParams = new OAuthResponseParams(request.getSecurityToken(), request,
fetcherConfig.getStateCrypter());
try {
return fetchNoThrow();
} catch (RuntimeException e) {
// We log here to record the request/response pairs that created the failure.
responseParams.logDetailedWarning(classname,"fetch",MessageKeys.OAUTH_FETCH_UNEXPECTED_ERROR, e);
throw e;
}
}
/**
* Fetch data and build a response to return to the client. We try to always return something
* reasonable to the calling app no matter what kind of madness happens along the way. If an
* unchecked exception occurs, well, then the client is out of luck.
*/
private HttpResponse fetchNoThrow() {
HttpResponseBuilder response;
try {
accessorInfo = fetcherConfig.getTokenStore().getOAuthAccessor(
realRequest.getSecurityToken(), realRequest.getOAuthArguments(), clientState,
responseParams, fetcherConfig);
response = fetchWithRetry();
} catch (OAuthRequestException e) {
// No data for us.
if (OAuthError.UNAUTHENTICATED.name().equals(e.getError())) {
responseParams.logDetailedInfo(classname,"fetchNoThrow",MessageKeys.UNAUTHENTICATED_OAUTH, e);
} else if (OAuthError.BAD_OAUTH_TOKEN_URL.name().equals(e.getError())) {
responseParams.logDetailedInfo(classname,"fetchNoThrow",MessageKeys.INVALID_OAUTH, e);
} else {
responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_FATAL_ERROR, e);
}
responseParams.setSendTraceToClient(true);
response = new HttpResponseBuilder()
.setHttpStatusCode(HttpResponse.SC_FORBIDDEN)
.setStrictNoCache();
responseParams.addToResponse(response, e);
return response.create();
}
// OK, got some data back, annotate it as necessary.
if (response.getHttpStatusCode() >= 400) {
responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_FATAL_ERROR);
responseParams.setSendTraceToClient(true);
} else if (responseParams.getAznUrl() != null && responseParams.sawErrorResponse()) {
responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_ERROR_REPROMPT);
responseParams.setSendTraceToClient(true);
}
responseParams.addToResponse(response, null);
return response.create();
}
/**
* Fetch data, retrying in the event that that the service provider returns an error and we think
* we can recover by restarting the protocol flow.
*/
private HttpResponseBuilder fetchWithRetry() throws OAuthRequestException {
int attempts = 0;
boolean retry;
HttpResponseBuilder response = null;
do {
retry = false;
++attempts;
try {
response = attemptFetch();
} catch (OAuthProtocolException pe) {
retry = handleProtocolException(pe, attempts);
if (!retry) {
if (pe.getProblemCode() != null) {
throw new OAuthRequestException(pe.getProblemCode(),
"Service provider rejected request", pe);
} else {
throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
"Service provider rejected request", pe);
}
}
}
} while (retry);
return response;
}
private boolean handleProtocolException(OAuthProtocolException pe, int attempts)
throws OAuthRequestException {
if (pe.canExtend()) {
accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_FORCE_EXPIRE);
} else if (pe.startFromScratch()) {
fetcherConfig.getTokenStore().removeToken(realRequest.getSecurityToken(),
accessorInfo.getConsumer(), realRequest.getOAuthArguments(), responseParams);
accessorInfo.getAccessor().accessToken = null;
accessorInfo.getAccessor().requestToken = null;
accessorInfo.getAccessor().tokenSecret = null;
accessorInfo.setSessionHandle(null);
accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
}
return (attempts < MAX_ATTEMPTS && pe.canRetry());
}
/**
* Does one of the following:
* 1) Sends a request token request, and returns an approval URL to the calling app.
* 2) Sends an access token request to swap a request token for an access token, and then asks
* for data from the service provider.
* 3) Asks for data from the service provider.
*/
private HttpResponseBuilder attemptFetch() throws OAuthRequestException, OAuthProtocolException {
if (needApproval()) {
// This is section 6.1 of the OAuth spec.
checkCanApprove();
fetchRequestToken();
// This is section 6.2 of the OAuth spec.
buildClientApprovalState();
buildAznUrl();
// break out of the content fetching chain, we need permission from
// the user to do this
return new HttpResponseBuilder()
.setHttpStatusCode(HttpResponse.SC_OK)
.setStrictNoCache();
} else if (needAccessToken()) {
// This is section 6.3 of the OAuth spec
checkCanApprove();
exchangeRequestToken();
saveAccessToken();
buildClientAccessState();
}
return fetchData();
}
/**
* Do we need to get the user's approval to access the data?
*/
private boolean needApproval() {
return (realRequest.getOAuthArguments().mustUseToken()
&& accessorInfo.getAccessor().requestToken == null
&& accessorInfo.getAccessor().accessToken == null);
}
/**
* Make sure the user is authorized to approve access tokens. At the moment
* we restrict this to page owner's viewing their own pages.
*/
private void checkCanApprove() throws OAuthRequestException {
String pageOwner = realRequest.getSecurityToken().getOwnerId();
String pageViewer = realRequest.getSecurityToken().getViewerId();
String stateOwner = clientState.getOwner();
if (pageOwner == null || pageViewer == null) {
throw new OAuthRequestException(OAuthError.UNAUTHENTICATED);
}
if (!fetcherConfig.isViewerAccessTokensEnabled() && !pageOwner.equals(pageViewer)) {
throw new OAuthRequestException(OAuthError.NOT_OWNER);
}
if (stateOwner != null && !stateOwner.equals(pageViewer)) {
throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
"Client state belongs to a different person " +
"(state owner=" + stateOwner + ", pageViewer=" + pageViewer + ')');
}
}
private void fetchRequestToken() throws OAuthRequestException, OAuthProtocolException {
OAuthAccessor accessor = accessorInfo.getAccessor();
HttpRequest request = createRequestTokenRequest(accessor);
List requestTokenParams = Lists.newArrayList();
addCallback(requestTokenParams);
HttpRequest signed = sanitizeAndSign(request, requestTokenParams, true);
OAuthMessage reply = sendOAuthMessage(signed);
accessor.requestToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
}
private HttpRequest createRequestTokenRequest(OAuthAccessor accessor)
throws OAuthRequestException {
if (accessor.consumer.serviceProvider.requestTokenURL == null) {
throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL, "request token");
}
HttpRequest request = new HttpRequest(
Uri.parse(accessor.consumer.serviceProvider.requestTokenURL));
request.setMethod(accessorInfo.getHttpMethod().toString());
if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
request.setHeader("Content-Type", OAuth.FORM_ENCODED);
}
return request;
}
private void addCallback(List requestTokenParams) throws OAuthRequestException {
// This will be either the consumer key callback URL or the global callback URL.
String baseCallback = StringUtils.trimToNull(accessorInfo.getConsumer().getCallbackUrl());
if (baseCallback != null) {
String callbackUrl = fetcherConfig.getOAuthCallbackGenerator().generateCallback(
fetcherConfig, baseCallback, realRequest, responseParams);
if (callbackUrl != null) {
requestTokenParams.add(new Parameter(OAuth.OAUTH_CALLBACK, callbackUrl));
}
}
}
/**
* Strip out any owner or viewer identity information passed by the client.
*/
private List sanitize(List params) throws OAuthRequestException {
ArrayList list = Lists.newArrayList();
for (Parameter p : params) {
String name = p.getKey();
if (allowParam(name)) {
list.add(p);
} else {
throw new OAuthRequestException(OAuthError.INVALID_PARAMETER, name);
}
}
return list;
}
protected boolean allowParam(String paramName) {
String canonParamName = paramName.toLowerCase();
return (!(canonParamName.startsWith("oauth") ||
canonParamName.startsWith("xoauth") ||
canonParamName.startsWith("opensocial")) &&
ALLOWED_PARAM_NAME.matcher(canonParamName).matches());
}
/**
* This gives a chance to override parameters by passing trusted parameters.
*
*/
private void overrideParameters(List authParams)
throws OAuthRequestException {
if (trustedParams == null) {
return;
}
Map paramMap = Maps.newLinkedHashMap();
for (Parameter param : authParams) {
paramMap.put(param.getKey(), param.getValue());
}
for (Parameter param : trustedParams) {
if (!isContainerInjectedParameter(param.getKey())) {
throw new OAuthRequestException(OAuthError.INVALID_TRUSTED_PARAMETER, param.getKey());
}
paramMap.put(param.getKey(), param.getValue());
}
authParams.clear();
for (Entry entry : paramMap.entrySet()) {
authParams.add(new Parameter(entry.getKey(), entry.getValue()));
}
}
/**
* Add identity information, such as owner/viewer/gadget.
*/
private void addIdentityParams(List params) {
// If no owner or viewer information is required, don't add any identity params. This lets
// us be compatible with strict OAuth service providers that reject extra parameters on
// requests.
if (!realRequest.getOAuthArguments().getSignOwner() &&
!realRequest.getOAuthArguments().getSignViewer()) {
return;
}
String owner = realRequest.getSecurityToken().getOwnerId();
if (owner != null && realRequest.getOAuthArguments().getSignOwner()) {
params.add(new Parameter(OPENSOCIAL_OWNERID, owner));
}
String viewer = realRequest.getSecurityToken().getViewerId();
if (viewer != null && realRequest.getOAuthArguments().getSignViewer()) {
params.add(new Parameter(OPENSOCIAL_VIEWERID, viewer));
}
String app = realRequest.getSecurityToken().getAppId();
if (app != null) {
params.add(new Parameter(OPENSOCIAL_APPID, app));
}
String appUrl = realRequest.getSecurityToken().getAppUrl();
if (appUrl != null) {
params.add(new Parameter(OPENSOCIAL_APPURL, appUrl));
}
if (realRequest.getOAuthArguments().isProxiedContentRequest()) {
params.add(new Parameter(OPENSOCIAL_PROXIED_CONTENT, "1"));
}
}
/**
* Add signature type to the message.
*/
private void addSignatureParams(List params) {
if (accessorInfo.getConsumer().getConsumer().consumerKey == null) {
params.add(
new Parameter(OAuth.OAUTH_CONSUMER_KEY, realRequest.getSecurityToken().getDomain()));
}
if (accessorInfo.getConsumer().getKeyName() != null) {
params.add(new Parameter(XOAUTH_PUBLIC_KEY_OLD, accessorInfo.getConsumer().getKeyName()));
params.add(new Parameter(XOAUTH_PUBLIC_KEY_NEW, accessorInfo.getConsumer().getKeyName()));
}
params.add(new Parameter(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0));
params.add(new Parameter(OAuth.OAUTH_TIMESTAMP,
Long.toString(fetcherConfig.getClock().currentTimeMillis() / 1000L)));
// the oauth.net java code uses a clock to generate nonces, which causes nonce collisions
// under heavy load. A random nonce is more reliable.
params.add(new Parameter(OAuth.OAUTH_NONCE, String.valueOf(Math.abs(Crypto.RAND.nextLong()))));
}
static String getAuthorizationHeader(List> oauthParams) {
StringBuilder result = new StringBuilder("OAuth ");
boolean first = true;
for (Map.Entry parameter : oauthParams) {
if (!first) {
result.append(", ");
} else {
first = false;
}
result.append(OAuth.percentEncode(parameter.getKey()))
.append("=\"")
.append(OAuth.percentEncode(parameter.getValue()))
.append('"');
}
return result.toString();
}
/**
* Start with an HttpRequest.
* Throw if there are any attacks in the query.
* Throw if there are any attacks in the post body.
* Build up OAuth parameter list.
* Sign it.
* Add OAuth parameters to new request.
* Send it.
*/
public HttpRequest sanitizeAndSign(HttpRequest base, List params,
boolean tokenEndpoint) throws OAuthRequestException {
if (params == null) {
params = Lists.newArrayList();
}
UriBuilder target = new UriBuilder(base.getUri());
String query = target.getQuery();
target.setQuery(null);
params.addAll(sanitize(OAuth.decodeForm(query)));
switch(OAuthUtil.getSignatureType(tokenEndpoint, base.getHeader("Content-Type"))) {
case URL_ONLY:
break;
case URL_AND_FORM_PARAMS:
try {
params.addAll(sanitize(OAuth.decodeForm(base.getPostBodyAsString())));
} catch (IllegalArgumentException e) {
// Occurs if OAuth.decodeForm finds an invalid URL to decode.
throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
"Could not decode body", e);
}
break;
case URL_AND_BODY_HASH:
try {
byte[] body = IOUtils.toByteArray(base.getPostBody());
byte[] hash = DigestUtils.sha(body);
String b64 = CharsetUtil.newUtf8String(Base64.encodeBase64(hash));
params.add(new Parameter(OAuthConstants.OAUTH_BODY_HASH, b64));
} catch (IOException e) {
throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
"Error taking body hash", e);
}
break;
}
// authParams are parameters prefixed with 'xoauth' 'oauth' or 'opensocial',
// trusted parameters have ability to override these parameters.
List authParams = Lists.newArrayList();
addIdentityParams(authParams);
addSignatureParams(authParams);
overrideParameters(authParams);
params.addAll(authParams);
try {
OAuthMessage signed = OAuthUtil.newRequestMessage(accessorInfo.getAccessor(),
base.getMethod(), target.toString(), params);
HttpRequest oauthHttpRequest = createHttpRequest(base, selectOAuthParams(signed));
// Following 302s on OAuth responses is unlikely to be productive.
oauthHttpRequest.setFollowRedirects(false);
return oauthHttpRequest;
} catch (OAuthException e) {
throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
"Error signing message", e);
}
}
private HttpRequest createHttpRequest(HttpRequest base,
List> oauthParams) throws OAuthRequestException {
OAuthParamLocation paramLocation = accessorInfo.getParamLocation();
// paramLocation could be overriden by a run-time parameter to fetchRequest
HttpRequest result = new HttpRequest(base);
// If someone specifies that OAuth parameters go in the body, but then sends a request for
// data using GET, we've got a choice. We can throw some type of error, since a GET request
// can't have a body, or we can stick the parameters somewhere else, like, say, the header.
// We opt to put them in the header, since that stands some chance of working with some
// OAuth service providers.
if (paramLocation == OAuthParamLocation.POST_BODY &&
!result.getMethod().equals("POST")) {
paramLocation = OAuthParamLocation.AUTH_HEADER;
}
switch (paramLocation) {
case AUTH_HEADER:
result.addHeader("Authorization", getAuthorizationHeader(oauthParams));
break;
case POST_BODY:
String contentType = result.getHeader("Content-Type");
if (!OAuth.isFormEncoded(contentType)) {
throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
"OAuth param location can only be post_body if it is of " +
"type x-www-form-urlencoded");
}
String oauthData = OAuthUtil.formEncode(oauthParams);
if (result.getPostBodyLength() == 0) {
result.setPostBody(CharsetUtil.getUtf8Bytes(oauthData));
} else {
StringBuilder postBody = new StringBuilder();
postBody.append(result.getPostBodyAsString());
if (!result.getPostBodyAsString().endsWith("&")) {
postBody.append('&');
}
postBody.append(oauthData);
result.setPostBody(postBody.toString().getBytes());
}
break;
case URI_QUERY:
result.setUri(Uri.parse(OAuthUtil.addParameters(result.getUri().toString(), oauthParams)));
break;
}
return result;
}
/**
* Sends OAuth request token and access token messages.
*/
private OAuthMessage sendOAuthMessage(HttpRequest request)
throws OAuthRequestException, OAuthProtocolException {
HttpResponse response = fetchFromServer(request);
checkForProtocolProblem(response);
OAuthMessage reply = new OAuthMessage(null, null, null);
reply.addParameters(OAuth.decodeForm(response.getResponseAsString()));
reply = parseAuthHeader(reply, response);
if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN) == null) {
throw new OAuthRequestException(OAuthError.MISSING_OAUTH_PARAMETER,
OAuth.OAUTH_TOKEN);
}
if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET) == null) {
throw new OAuthRequestException(OAuthError.MISSING_OAUTH_PARAMETER,
OAuth.OAUTH_TOKEN_SECRET);
}
return reply;
}
/**
* Parse OAuth WWW-Authenticate header and either add them to an existing
* message or create a new message.
*
* @param msg
* @param resp
* @return the updated message.
*/
private OAuthMessage parseAuthHeader(OAuthMessage msg, HttpResponse resp) {
if (msg == null) {
msg = new OAuthMessage(null, null, null);
}
for (String auth : resp.getHeaders("WWW-Authenticate")) {
msg.addParameters(OAuthMessage.decodeAuthorization(auth));
}
return msg;
}
/**
* Builds the data we'll cache on the client while we wait for approval.
*/
private void buildClientApprovalState() {
OAuthAccessor accessor = accessorInfo.getAccessor();
responseParams.getNewClientState().setRequestToken(accessor.requestToken);
responseParams.getNewClientState().setRequestTokenSecret(accessor.tokenSecret);
responseParams.getNewClientState().setOwner(realRequest.getSecurityToken().getOwnerId());
}
/**
* Builds the URL the client needs to visit to approve access.
*/
private void buildAznUrl() throws OAuthRequestException {
// We add the token, gadget is responsible for the callback URL.
OAuthAccessor accessor = accessorInfo.getAccessor();
if (accessor.consumer.serviceProvider.userAuthorizationURL == null) {
throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL,
"authorization");
}
StringBuilder azn = new StringBuilder(
accessor.consumer.serviceProvider.userAuthorizationURL);
if (azn.indexOf("?") == -1) {
azn.append('?');
} else {
azn.append('&');
}
azn.append(OAuth.OAUTH_TOKEN);
azn.append('=');
azn.append(OAuth.percentEncode(accessor.requestToken));
responseParams.setAznUrl(azn.toString());
}
/**
* Do we need to exchange a request token for an access token?
*/
private boolean needAccessToken() {
if (realRequest.getOAuthArguments().mustUseToken()
&& accessorInfo.getAccessor().requestToken != null
&& accessorInfo.getAccessor().accessToken == null) {
return true;
}
return realRequest.getOAuthArguments().mayUseToken() && accessTokenExpired();
}
private boolean accessTokenExpired() {
return (accessorInfo.getTokenExpireMillis() != ACCESS_TOKEN_EXPIRE_UNKNOWN
&& accessorInfo.getTokenExpireMillis() < fetcherConfig.getClock().currentTimeMillis());
}
/**
* Implements section 6.3 of the OAuth spec.
*/
private void exchangeRequestToken() throws OAuthRequestException, OAuthProtocolException {
if (accessorInfo.getAccessor().accessToken != null) {
// session extension per
// http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html
accessorInfo.getAccessor().requestToken = accessorInfo.getAccessor().accessToken;
accessorInfo.getAccessor().accessToken = null;
}
OAuthAccessor accessor = accessorInfo.getAccessor();
if (accessor.consumer.serviceProvider.accessTokenURL == null) {
throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL, "access token");
}
Uri accessTokenUri = Uri.parse(accessor.consumer.serviceProvider.accessTokenURL);
HttpRequest request = new HttpRequest(accessTokenUri);
request.setMethod(accessorInfo.getHttpMethod().toString());
if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
request.setHeader("Content-Type", OAuth.FORM_ENCODED);
}
List msgParams = Lists.newArrayList();
msgParams.add(new Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
if (accessorInfo.getSessionHandle() != null) {
msgParams.add(new Parameter(OAuthConstants.OAUTH_SESSION_HANDLE,
accessorInfo.getSessionHandle()));
}
String receivedCallback = realRequest.getOAuthArguments().getReceivedCallbackUrl();
if (!StringUtils.isBlank(receivedCallback)) {
try {
Uri parsed = Uri.parse(receivedCallback);
String verifier = parsed.getQueryParameter(OAuth.OAUTH_VERIFIER);
if (verifier != null) {
msgParams.add(new Parameter(OAuth.OAUTH_VERIFIER, verifier));
}
} catch (IllegalArgumentException e) {
throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
"Invalid received callback URL: " + receivedCallback, e);
}
}
HttpRequest signed = sanitizeAndSign(request, msgParams, true);
OAuthMessage reply = sendOAuthMessage(signed);
accessor.accessToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
accessorInfo.setSessionHandle(OAuthUtil.getParameter(reply,
OAuthConstants.OAUTH_SESSION_HANDLE));
accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
if (OAuthUtil.getParameter(reply, OAuthConstants.OAUTH_EXPIRES_IN) != null) {
try {
int expireSecs = Integer.parseInt(OAuthUtil.getParameter(reply,
OAuthConstants.OAUTH_EXPIRES_IN));
long expireMillis = fetcherConfig.getClock().currentTimeMillis() + expireSecs * 1000L;
accessorInfo.setTokenExpireMillis(expireMillis);
} catch (NumberFormatException e) {
// Hrm. Bogus server. We can safely ignore this, we'll just wait for the server to
// tell us when the access token has expired.
responseParams.logDetailedWarning(classname,"exchangeRequestToken",MessageKeys.BOGUS_EXPIRED);
}
}
// Clients may want to retrieve extra information returned with the access token. Several
// OAuth service providers (e.g. Yahoo, NetFlix) return a user id along with the access
// token, and the user id is required to use their APIs. Clients signal that they need this
// extra data by sending a fetch request for the access token URL.
//
// We don't return oauth* parameters from the response, because we know how to handle those
// ourselves and some of them (such as oauth_token_secret) aren't supposed to be sent to the
// client.
//
// Note that this data is not stored server-side. Clients need to cache these user-ids or
// other data themselves, probably in user prefs, if they expect to need the data in the
// future.
if (accessTokenUri.equals(realRequest.getUri())) {
accessTokenData = Maps.newHashMap();
for (Entry param : OAuthUtil.getParameters(reply)) {
if (!param.getKey().startsWith("oauth")) {
accessTokenData.put(param.getKey(), param.getValue());
}
}
}
}
/**
* Save off our new token and secret to the persistent store.
*/
private void saveAccessToken() throws OAuthRequestException {
OAuthAccessor accessor = accessorInfo.getAccessor();
TokenInfo tokenInfo = new TokenInfo(accessor.accessToken, accessor.tokenSecret,
accessorInfo.getSessionHandle(), accessorInfo.getTokenExpireMillis());
fetcherConfig.getTokenStore().storeTokenKeyAndSecret(realRequest.getSecurityToken(),
accessorInfo.getConsumer(), realRequest.getOAuthArguments(), tokenInfo, responseParams);
}
/**
* Builds the data we'll cache on the client while we make requests.
*/
private void buildClientAccessState() {
OAuthAccessor accessor = accessorInfo.getAccessor();
responseParams.getNewClientState().setAccessToken(accessor.accessToken);
responseParams.getNewClientState().setAccessTokenSecret(accessor.tokenSecret);
responseParams.getNewClientState().setOwner(realRequest.getSecurityToken().getOwnerId());
responseParams.getNewClientState().setSessionHandle(accessorInfo.getSessionHandle());
responseParams.getNewClientState().setTokenExpireMillis(accessorInfo.getTokenExpireMillis());
}
/**
* Get honest-to-goodness user data.
*
* @throws OAuthProtocolException if the service provider returns an OAuth
* related error instead of user data.
*/
private HttpResponseBuilder fetchData() throws OAuthRequestException, OAuthProtocolException {
HttpResponseBuilder builder;
if (accessTokenData != null) {
// This is a request for access token data, return it.
builder = formatAccessTokenData();
} else {
HttpRequest signed = sanitizeAndSign(realRequest, null, false);
HttpResponse response = fetchFromServer(signed);
checkForProtocolProblem(response);
builder = new HttpResponseBuilder(response);
}
return builder;
}
private HttpResponse fetchFromServer(HttpRequest request) throws OAuthRequestException {
HttpResponse response = null;
try {
response = fetcher.fetch(request);
if (response == null) {
throw new OAuthRequestException(OAuthError.MISSING_SERVER_RESPONSE);
}
return response;
} catch (GadgetException e) {
throw new OAuthRequestException(OAuthError.MISSING_SERVER_RESPONSE, "", e);
} finally {
responseParams.addRequestTrace(request, response);
}
}
/**
* Access token data is returned to the gadget as json key/value pairs:
*
* { "user_id": "12345678" }
*/
private HttpResponseBuilder formatAccessTokenData() {
HttpResponseBuilder builder = new HttpResponseBuilder();
builder.addHeader("Content-Type", "application/json; charset=utf-8");
builder.setHttpStatusCode(HttpResponse.SC_OK);
// no need to cache this, these requests should be fairly rare, and the results should be
// cached in gadget.
builder.setStrictNoCache();
JSONObject json = new JSONObject(accessTokenData);
builder.setResponseString(json.toString());
return builder;
}
/**
* Look for an OAuth protocol problem. For cases where no access token is in play
* @param response
* @throws OAuthProtocolException
*/
private void checkForProtocolProblem(HttpResponse response) throws OAuthProtocolException {
if (couldBeFullOAuthError(response)) {
// OK, might be OAuth related.
OAuthMessage message = parseAuthHeader(null, response);
if (OAuthUtil.getParameter(message, OAuthProblemException.OAUTH_PROBLEM) != null) {
// SP reported extended error information
throw new OAuthProtocolException(response.getHttpStatusCode(), message);
}
// No extended information, guess based on HTTP response code.
if (response.getHttpStatusCode() == HttpResponse.SC_UNAUTHORIZED) {
throw new OAuthProtocolException(response.getHttpStatusCode());
}
}
}
/**
* Check if a response might be due to an OAuth protocol error. We don't want to intercept
* errors for signed fetch, we only care about places where we are dealing with OAuth request
* and/or access tokens.
*/
private boolean couldBeFullOAuthError(HttpResponse response) {
// 400, 401 and 403 are likely to be authentication errors. Unfortunately there is
// significant overlap with other types of server errors as well, so we can't just assume
// that the root cause of these errors is a bad token or a bad consumer key.
if (response.getHttpStatusCode() != HttpResponse.SC_BAD_REQUEST
&& response.getHttpStatusCode() != HttpResponse.SC_UNAUTHORIZED
&& response.getHttpStatusCode() != HttpResponse.SC_FORBIDDEN) {
return false;
}
// If the client forced us to use full OAuth, this might be OAuth related.
if (realRequest.getOAuthArguments().mustUseToken()) {
return true;
}
// If we're using an access token, this might be OAuth related.
if (accessorInfo.getAccessor().accessToken != null) {
return true;
}
// Not OAuth related.
return false;
}
/**
* Extracts only those parameters from an OAuthMessage that are OAuth-related.
* An OAuthMessage may hold a whole bunch of non-OAuth-related parameters
* because they were all needed for signing. But when constructing a request
* we need to be able to extract just the OAuth-related parameters because
* they, and only they, may have to be put into an Authorization: header or
* some such thing.
*
* @param message the OAuthMessage object, which holds non-OAuth parameters
* such as foo=bar (which may have been in the original URI query part, or
* perhaps in the POST body), as well as OAuth-related parameters (such as
* oauth_timestamp or oauth_signature).
*
* @return a list that contains only the oauth_related parameters.
*/
static List> selectOAuthParams(OAuthMessage message) {
List> result = Lists.newArrayList();
for (Map.Entry param : OAuthUtil.getParameters(message)) {
if (isContainerInjectedParameter(param.getKey())) {
result.add(param);
}
}
return result;
}
protected static boolean isContainerInjectedParameter(String key) {
key = key.toLowerCase();
return key.startsWith("oauth") || key.startsWith("xoauth") || key.startsWith("opensocial");
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy