Author: beaton
Date: Sun Nov 9 22:09:04 2008
New Revision: 712613
URL: http://svn.apache.org/viewvc?rev=712613&view=rev
Log:
Compatibility with Yahoo and NetFlix OAuth APIs.
For Yahoo: provide a mechanism to disable sending opensocial params for
pure OAuth requests, since their request and access token endpoints are
strict on that topic.
For both Yahoo and Netflix: provide a mechanism to request extra data
returned from the access token URL. Both Yahoo and Netflix return a
user-id along with the access token, and require that you specify the
user-id in subsequent API calls.
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcher.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherTest.java
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcher.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcher.java?rev=712613&r1=712612&r2=712613&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcher.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcher.java
Sun Nov 9 22:09:04 2008
@@ -46,10 +46,13 @@
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
+import org.json.JSONObject;
+
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
@@ -127,6 +130,11 @@
* The request the client really wants to make.
*/
private HttpRequest realRequest;
+
+ /**
+ * Data returned along with OAuth access token, null if this is not an
access token request
+ */
+ private Map<String, String> accessTokenData;
/**
* @param fetcherConfig configuration options for the fetcher
@@ -354,6 +362,14 @@
* Add identity information, such as owner/viewer/gadget.
*/
private void addIdentityParams(List<Parameter> 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));
@@ -373,17 +389,16 @@
if (appUrl != null) {
params.add(new Parameter(OPENSOCIAL_APPURL, appUrl));
}
-
- if (accessorInfo.getConsumer().getConsumer().consumerKey == null) {
- params.add(
- new Parameter(OAuth.OAUTH_CONSUMER_KEY,
realRequest.getSecurityToken().getDomain()));
- }
}
/**
* Add signature type to the message.
*/
private void addSignatureParams(List<Parameter> 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,
accessorInfo.getConsumer().getKeyName()));
}
@@ -615,8 +630,8 @@
accessorInfo.getAccessor().accessToken = null;
}
OAuthAccessor accessor = accessorInfo.getAccessor();
- HttpRequest request = new HttpRequest(
- Uri.parse(accessor.consumer.serviceProvider.accessTokenURL));
+ 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);
@@ -647,6 +662,27 @@
logger.log(Level.WARNING, "server returned bogus expiration: " +
reply);
}
}
+
+ // 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<String, String> param : OAuthUtil.getParameters(reply)) {
+ if (!param.getKey().startsWith("oauth")) {
+ accessTokenData.put(param.getKey(), param.getValue());
+ }
+ }
+ }
} catch (OAuthException e) {
throw new UserVisibleOAuthException(e.getMessage(), e);
}
@@ -684,22 +720,44 @@
* related error instead of user data.
*/
private HttpResponse fetchData() throws GadgetException,
OAuthProtocolException {
- HttpRequest signed = sanitizeAndSign(realRequest, null);
+ HttpResponseBuilder builder = null;
+ if (accessTokenData != null) {
+ // This is a request for access token data, return it.
+ builder = formatAccessTokenData();
+ } else {
+ HttpRequest signed = sanitizeAndSign(realRequest, null);
- HttpResponse response = nextFetcher.fetch(signed);
+ HttpResponse response = nextFetcher.fetch(signed);
- try {
- checkForProtocolProblem(response);
- } catch (OAuthProtocolException e) {
- logServiceProviderError(signed, response);
- throw e;
+ try {
+ checkForProtocolProblem(response);
+ } catch (OAuthProtocolException e) {
+ logServiceProviderError(signed, response);
+ throw e;
+ }
+ builder = new HttpResponseBuilder(response);
}
-
// Track metadata on the response
- HttpResponseBuilder builder = new HttpResponseBuilder(response);
responseParams.addToResponse(builder);
return builder.create();
}
+
+ /**
+ * 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
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java?rev=712613&r1=712612&r2=712613&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
Sun Nov 9 22:09:04 2008
@@ -34,6 +34,7 @@
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.OAuthUtil;
import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
import java.io.ByteArrayOutputStream;
@@ -44,6 +45,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.Map.Entry;
public class FakeOAuthServiceProvider implements HttpFetcher {
@@ -167,6 +169,8 @@
private boolean vagueErrors = false;
private boolean reportExpirationTimes = true;
private boolean sessionExtension = false;
+ private boolean rejectExtraParams = false;
+ private boolean returnAccessTokenData = false;
private int requestTokenCount = 0;
@@ -204,6 +208,14 @@
this.reportExpirationTimes = reportExpirationTimes;
}
+ public void setRejectExtraParams(boolean rejectExtraParams) {
+ this.rejectExtraParams = rejectExtraParams;
+ }
+
+ public void setReturnAccessTokenData(boolean returnAccessTokenData) {
+ this.returnAccessTokenData = returnAccessTokenData;
+ }
+
public void addParamLocation(OAuthParamLocation paramLocation) {
validParamLocations.add(paramLocation);
}
@@ -217,9 +229,7 @@
validParamLocations.add(paramLocation);
}
- @SuppressWarnings("unused")
- public HttpResponse fetch(HttpRequest request)
- throws GadgetException {
+ public HttpResponse fetch(HttpRequest request) throws GadgetException {
return realFetch(request);
}
@@ -262,6 +272,12 @@
return makeOAuthProblemReport(
"consumer_key_refused", "exceeded quota exhausted");
}
+ if (rejectExtraParams) {
+ String extra = hasExtraParams(message);
+ if (extra != null) {
+ return makeOAuthProblemReport("parameter_rejected", extra);
+ }
+ }
OAuthAccessor accessor = new OAuthAccessor(consumer);
message.validateMessage(accessor, validator);
String requestToken = Crypto.getRandomString(16);
@@ -274,6 +290,16 @@
return new HttpResponse(resp);
}
+ private String hasExtraParams(OAuthMessage message) {
+ for (Entry<String, String> param : OAuthUtil.getParameters(message)) {
+ // Our request token URL allows "param" as a query param, and also oauth
params of course.
+ if (!param.getKey().startsWith("oauth") &&
!param.getKey().equals("param")) {
+ return param.getKey();
+ }
+ }
+ return null;
+ }
+
private HttpResponse makeOAuthProblemReport(String code, String text) throws
IOException {
if (vagueErrors) {
int rc = HttpResponse.SC_UNAUTHORIZED;
@@ -488,6 +514,12 @@
"consumer_key_refused", "exceeded quota");
} else if (state == null) {
return makeOAuthProblemReport("token_rejected", "Unknown request token");
+ }
+ if (rejectExtraParams) {
+ String extra = hasExtraParams(message);
+ if (extra != null) {
+ return makeOAuthProblemReport("parameter_rejected", extra);
+ }
}
OAuthAccessor accessor = new OAuthAccessor(oauthConsumer);
@@ -501,7 +533,7 @@
// Verify can refresh
String sentHandle = message.getParameter("oauth_session_handle");
if (sentHandle == null) {
- throw new Exception("No oauth_session_handle");
+ return makeOAuthProblemReport("parameter_absent", "no
oauth_session_handle");
}
if (!sentHandle.equals(state.sessionHandle)) {
return makeOAuthProblemReport("token_invalid", "token not valid");
@@ -527,6 +559,11 @@
params.add(new OAuth.Parameter("oauth_expires_in", "" +
TOKEN_EXPIRATION_SECONDS));
}
}
+ if (returnAccessTokenData) {
+ params.add(new OAuth.Parameter("userid", "userid value"));
+ params.add(new OAuth.Parameter("xoauth_stuff", "xoauth_stuff value"));
+ params.add(new OAuth.Parameter("oauth_stuff", "oauth_stuff value"));
+ }
return new HttpResponse(OAuth.formEncode(params));
}
Modified:
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherTest.java
URL:
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherTest.java?rev=712613&r1=712612&r2=712613&view=diff
==============================================================================
---
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherTest.java
(original)
+++
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherTest.java
Sun Nov 9 22:09:04 2008
@@ -48,6 +48,7 @@
import net.oauth.OAuth.Parameter;
import org.apache.commons.codec.binary.Base64;
+import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -214,9 +215,21 @@
public void tearDown() throws Exception {
}
+ /** Client that does OAuth and sends opensocial_* params */
private MakeRequestClient makeNonSocialClient(String owner, String viewer,
String gadget)
throws Exception {
SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
+ MakeRequestClient client = new MakeRequestClient(securityToken,
fetcherConfig, serviceProvider,
+ FakeGadgetSpecFactory.SERVICE_NAME);
+ client.getBaseArgs().setSignOwner(true);
+ client.getBaseArgs().setSignViewer(true);
+ return client;
+ }
+
+ /** Client that does OAuth and does not send opensocial_* params */
+ private MakeRequestClient makeStrictNonSocialClient(String owner, String
viewer, String gadget)
+ throws Exception {
+ SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
return new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
FakeGadgetSpecFactory.SERVICE_NAME);
}
@@ -1116,6 +1129,86 @@
assertEquals("User data is renewed", response.getResponseAsString());
}
+ @Test
+ public void testExtraParamsRejected() throws Exception {
+ serviceProvider.setRejectExtraParams(true);
+ MakeRequestClient client = makeNonSocialClient("owner", "owner",
GADGET_URL);
+
+ HttpResponse response =
client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("parameter_rejected",
response.getMetadata().get("oauthError"));
+ }
+
+ @Test
+ public void testExtraParamsSuppressed() throws Exception {
+ serviceProvider.setRejectExtraParams(true);
+ MakeRequestClient client = makeStrictNonSocialClient("owner", "owner",
GADGET_URL);
+
+ HttpResponse response =
client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("", response.getResponseAsString());
+ client.approveToken("user_data=hello-oauth");
+
+ response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("User data is hello-oauth", response.getResponseAsString());
+ }
+
+ @Test
+ public void testCanRetrieveAccessTokenData() throws Exception {
+ serviceProvider.setReturnAccessTokenData(true);
+
+ MakeRequestClient client = makeNonSocialClient("owner", "owner",
GADGET_URL);
+
+ HttpResponse response =
client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ assertEquals("", response.getResponseAsString());
+ client.approveToken("user_data=hello-oauth");
+
+ response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ assertEquals("application/json; charset=utf-8",
response.getHeader("Content-Type"));
+ JSONObject json = new JSONObject(response.getResponseAsString());
+ assertEquals("userid value", json.get("userid"));
+ assertEquals("xoauth_stuff value", json.get("xoauth_stuff"));
+
+ response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("User data is hello-oauth", response.getResponseAsString());
+ }
+
+ @Test
+ public void testAccessTokenData_noOAuthParams() throws Exception {
+ serviceProvider.setReturnAccessTokenData(true);
+
+ MakeRequestClient client = makeNonSocialClient("owner", "owner",
GADGET_URL);
+
+ HttpResponse response =
client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ assertEquals("", response.getResponseAsString());
+ client.approveToken("user_data=hello-oauth");
+
+ response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ JSONObject json = new JSONObject(response.getResponseAsString());
+ assertEquals("userid value", json.get("userid"));
+ assertEquals("xoauth_stuff value", json.get("xoauth_stuff"));
+ assertEquals(2, json.length());
+
+ response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("User data is hello-oauth", response.getResponseAsString());
+ }
+
+ @Test
+ public void testAccessTokenData_noDirectRequest() throws Exception {
+ serviceProvider.setReturnAccessTokenData(true);
+
+ MakeRequestClient client = makeNonSocialClient("owner", "owner",
GADGET_URL);
+
+ HttpResponse response =
client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ assertEquals("", response.getResponseAsString());
+ client.approveToken("user_data=hello-oauth");
+
+ response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+ assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+ response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+ assertEquals("", response.getResponseAsString());
+ assertTrue(response.getMetadata().containsKey("oauthApprovalUrl"));
+ }
+
// Checks whether the given parameter list contains the specified
// key/value pair
private boolean contains(List<Parameter> params, String key, String value) {