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) {


Reply via email to