Author: beaton
Date: Mon Mar 23 21:26:18 2009
New Revision: 757551

URL: http://svn.apache.org/viewvc?rev=757551&view=rev
Log:
Outbound signing of non form-encoded request bodies.

Modified:
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
    
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java?rev=757551&r1=757550&r2=757551&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
 Mon Mar 23 21:26:18 2009
@@ -16,6 +16,9 @@
  */
 package org.apache.shindig.gadgets.oauth;
 
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.uri.UriBuilder;
 import org.apache.shindig.common.util.CharsetUtil;
@@ -41,6 +44,7 @@
 
 import org.json.JSONObject;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -87,6 +91,8 @@
   private static final String OAUTH_SESSION_HANDLE = "oauth_session_handle";
 
   private static final String OAUTH_EXPIRES_IN = "oauth_expires_in";
+  
+  private static final String OAUTH_BODY_HASH = "oauth_body_hash";
 
   private static final long ACCESS_TOKEN_EXPIRE_UNKNOWN = 0;
   private static final long ACCESS_TOKEN_FORCE_EXPIRE = -1;
@@ -455,8 +461,24 @@
     String query = target.getQuery();
     target.setQuery(null);
     params.addAll(sanitize(OAuth.decodeForm(query)));
-    if (OAuth.isFormEncoded(base.getHeader("Content-Type"))) {
-      params.addAll(sanitize(OAuth.decodeForm(base.getPostBodyAsString())));
+
+    switch(OAuthUtil.getSignatureType(base)) {
+      case URL_ONLY:
+        break;
+      case URL_AND_FORM_PARAMS:
+        params.addAll(sanitize(OAuth.decodeForm(base.getPostBodyAsString())));
+        break;
+      case URL_AND_BODY_HASH:
+        try {
+          byte[] body = IOUtils.toByteArray(base.getPostBody());
+          byte[] hash = DigestUtils.sha(body);
+          String b64 = new String(Base64.encodeBase64(hash), 
CharsetUtil.UTF8.name());
+          params.add(new Parameter(OAUTH_BODY_HASH, b64));
+        } catch (IOException e) {
+          throw 
responseParams.oauthRequestException(OAuthError.UNKNOWN_PROBLEM,
+              "Error taking body hash", e);
+        }
+        break;
     }
 
     addIdentityParams(params);

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java?rev=757551&r1=757550&r2=757551&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthUtil.java
 Mon Mar 23 21:26:18 2009
@@ -25,6 +25,8 @@
 import net.oauth.OAuthMessage;
 import net.oauth.OAuth.Parameter;
 
+import org.apache.shindig.gadgets.http.HttpRequest;
+
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.List;
@@ -79,4 +81,20 @@
       throw new RuntimeException(e);
     }
   }
+  
+  public static enum SignatureType {
+    URL_ONLY,
+    URL_AND_FORM_PARAMS,
+    URL_AND_BODY_HASH,
+  }
+  
+  public static SignatureType getSignatureType(HttpRequest request) {
+    if (OAuth.isFormEncoded(request.getHeader("Content-Type"))) {
+      return SignatureType.URL_AND_FORM_PARAMS;
+    }
+    if ("GET".equals(request.getMethod()) || 
"HEAD".equals(request.getMethod())) {
+      return SignatureType.URL_ONLY;
+    }
+    return SignatureType.URL_AND_BODY_HASH;
+  }
 }

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=757551&r1=757550&r2=757551&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
 Mon Mar 23 21:26:18 2009
@@ -24,14 +24,17 @@
 import net.oauth.OAuth;
 import net.oauth.OAuthAccessor;
 import net.oauth.OAuthConsumer;
+import net.oauth.OAuthException;
 import net.oauth.OAuthMessage;
 import net.oauth.OAuthServiceProvider;
-import net.oauth.OAuthValidator;
 import net.oauth.SimpleOAuthValidator;
 import net.oauth.signature.RSA_SHA1;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
 import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.util.CharsetUtil;
 import org.apache.shindig.common.util.TimeSource;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.http.HttpFetcher;
@@ -41,9 +44,9 @@
 import org.apache.shindig.gadgets.oauth.OAuthUtil;
 import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
 
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
 import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Set;
@@ -164,7 +167,6 @@
    * Table of OAuth access tokens
    */
   private final HashMap<String, TokenState> tokenState;
-  private final OAuthValidator validator;
   private final OAuthConsumer signedFetchConsumer;
   private final OAuthConsumer oauthConsumer;
   private final TimeSource clock;
@@ -205,7 +207,6 @@
     oauthConsumer = new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET, 
provider);
 
     tokenState = Maps.newHashMap();
-    validator = new FakeTimeOAuthValidator();
     validParamLocations = Sets.newHashSet();
     validParamLocations.add(OAuthParamLocation.URI_QUERY);
   }
@@ -280,8 +281,8 @@
 
   private HttpResponse handleRequestTokenUrl(HttpRequest request)
       throws Exception {
-    OAuthMessage message = parseMessage(request).message;
-    String requestConsumer = message.getParameter(OAuth.OAUTH_CONSUMER_KEY);
+    MessageInfo info = parseMessage(request);
+    String requestConsumer = 
info.message.getParameter(OAuth.OAUTH_CONSUMER_KEY);
     OAuthConsumer consumer;
     if (CONSUMER_KEY.equals(requestConsumer)) {
       consumer = oauthConsumer;
@@ -294,13 +295,13 @@
           "consumer_key_refused", "exceeded quota exhausted");
     }
     if (rejectExtraParams) {
-      String extra = hasExtraParams(message);
+      String extra = hasExtraParams(info.message);
       if (extra != null) {
         return makeOAuthProblemReport("parameter_rejected", extra);
       }
     }
     OAuthAccessor accessor = new OAuthAccessor(consumer);
-    message.validateMessage(accessor, validator);
+    validateMessage(accessor, info);
     String requestToken = Crypto.getRandomString(16);
     String requestTokenSecret = Crypto.getRandomString(16);
     tokenState.put(
@@ -345,6 +346,7 @@
   // to the OAuth specification
   private MessageInfo parseMessage(HttpRequest request) {
     MessageInfo info = new MessageInfo();
+    info.request = request;
     String method = request.getMethod();
     ParsedUrl parsed = new ParsedUrl(request.getUri().toString());
 
@@ -374,9 +376,8 @@
     }
 
     // Parse body
-    if (request.getMethod().equals("POST")) {
-      String type = request.getHeader("Content-Type");
-      if ("application/x-www-form-urlencoded".equals(type)) {
+    switch(OAuthUtil.getSignatureType(request)) {
+      case URL_AND_FORM_PARAMS:
         String body = request.getPostBodyAsString();
         info.body = body;
         params.addAll(OAuth.decodeForm(request.getPostBodyAsString()));
@@ -387,20 +388,16 @@
             throw new RuntimeException("Found unexpected post body data" + 
body);
           }
         }
-      } else {
+        break;
+      case URL_AND_BODY_HASH:
         try {
-          InputStream is = request.getPostBody();
-          ByteArrayOutputStream baos = new ByteArrayOutputStream();
-          byte[] buf = new byte[1024];
-          int read;
-          while ((read = is.read(buf, 0, buf.length)) != -1) {
-            baos.write(buf, 0, read);
-          }
-          info.rawBody = baos.toByteArray();
+          info.rawBody = IOUtils.toByteArray(request.getPostBody());
         } catch (IOException e) {
           throw new RuntimeException(e);
         }
-      }
+        break;
+      case URL_ONLY:
+        break;
     }
 
     // Return the lot
@@ -431,6 +428,7 @@
     public String aznHeader;
     public String body;
     public byte[] rawBody;
+    public HttpRequest request;
   }
 
   /**
@@ -542,8 +540,8 @@
 
   private HttpResponse handleAccessTokenUrl(HttpRequest request)
       throws Exception {
-    OAuthMessage message = parseMessage(request).message;
-    String requestToken = message.getParameter("oauth_token");
+    MessageInfo info = parseMessage(request);
+    String requestToken = info.message.getParameter("oauth_token");
     TokenState state = tokenState.get(requestToken);
     if (throttled) {
       return makeOAuthProblemReport(
@@ -552,7 +550,7 @@
       return makeOAuthProblemReport("token_rejected", "Unknown request token");
     }   
     if (rejectExtraParams) {
-      String extra = hasExtraParams(message);
+      String extra = hasExtraParams(info.message);
       if (extra != null) {
         return makeOAuthProblemReport("parameter_rejected", extra);
       }
@@ -561,13 +559,13 @@
     OAuthAccessor accessor = new OAuthAccessor(oauthConsumer);
     accessor.requestToken = requestToken;
     accessor.tokenSecret = state.tokenSecret;
-    message.validateMessage(accessor, validator);
+    validateMessage(accessor, info);
 
     if (state.getState() == State.APPROVED_UNCLAIMED) {
       state.claimToken();
     } else if (state.getState() == State.APPROVED) {
       // Verify can refresh
-      String sentHandle = message.getParameter("oauth_session_handle");
+      String sentHandle = info.message.getParameter("oauth_session_handle");
       if (sentHandle == null) {
         return makeOAuthProblemReport("parameter_absent", "no 
oauth_session_handle");
       }
@@ -634,7 +632,7 @@
       // Check the signature
       accessor.accessToken = accessToken;
       accessor.tokenSecret = state.getSecret();
-      info.message.validateMessage(accessor, validator);
+      validateMessage(accessor, info);
 
       if (state.getState() != State.APPROVED) {
         return makeOAuthProblemReport(
@@ -649,7 +647,7 @@
       responseBody = "User data is " + state.getUserData();
     } else {
       // Check the signature
-      info.message.validateMessage(accessor, validator);
+      validateMessage(accessor, info);
 
       // For signed fetch, just echo back the query parameters in the body
       responseBody = request.getUri().getQuery();
@@ -671,6 +669,30 @@
     }
     return resp.create();
   }
+  
+  private void validateMessage(OAuthAccessor accessor, MessageInfo info)
+      throws OAuthException, IOException, URISyntaxException {
+    info.message.validateMessage(accessor, new FakeTimeOAuthValidator());
+    String bodyHash = info.message.getParameter("oauth_body_hash");
+    switch (OAuthUtil.getSignatureType(info.request)) {
+      case URL_ONLY:
+        break;
+      case URL_AND_FORM_PARAMS:
+        if (bodyHash != null) {
+          throw new RuntimeException("Can't have body hash in form-encoded 
request");
+        }
+        break;
+      case URL_AND_BODY_HASH:
+        if (bodyHash == null) {
+          throw new RuntimeException("Requiring oauth_body_hash parameter");
+        }
+        byte[] received = 
Base64.decodeBase64(CharsetUtil.getUtf8Bytes(bodyHash));
+        byte[] expected = DigestUtils.sha(info.rawBody);
+        if (!Arrays.equals(received, expected)) {
+          throw new RuntimeException("oauth_body_hash mismatch");
+        }
+    }
+  }
 
   private HttpResponse handleNotFoundUrl(HttpRequest request) throws Exception 
{
     return new HttpResponseBuilder()

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java?rev=757551&r1=757550&r2=757551&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
 Mon Mar 23 21:26:18 2009
@@ -25,6 +25,7 @@
 import org.apache.shindig.auth.SecurityToken;
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.util.CharsetUtil;
+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.oauth.OAuthArguments;
@@ -54,6 +55,7 @@
   private String approvalUrl;
   private boolean ignoreCache;
   private Map<String, String> trustedParams = Maps.newHashMap();
+  private HttpFetcher nextFetcher;
 
   /**
    * Create a make request client with the given security token, sending 
requests through an
@@ -88,20 +90,28 @@
   public void setIgnoreCache(boolean ignoreCache) {
     this.ignoreCache = ignoreCache;
   }
+  
+  public void setNextFetcher(HttpFetcher nextFetcher) {
+    this.nextFetcher = nextFetcher;
+  }
 
   public void setTrustedParam(String name, String value) {
     trustedParams.put(name, value);
   }
 
   private OAuthRequest createRequest() {
+    HttpFetcher dest = serviceProvider;
+    if (nextFetcher != null) {
+      dest = nextFetcher;
+    }
     if (trustedParams != null) {
       List<Parameter> trusted = Lists.newArrayList();
       for (Entry<String, String> e : trustedParams.entrySet()) {
         trusted.add(new Parameter(e.getKey(), e.getValue()));
       }
-      return new OAuthRequest(fetcherConfig, serviceProvider, trusted);
+      return new OAuthRequest(fetcherConfig, dest, trusted);
     }
-    return new OAuthRequest(fetcherConfig, serviceProvider);
+    return new OAuthRequest(fetcherConfig, dest);
   }
 
   /**

Modified: 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java?rev=757551&r1=757550&r2=757551&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
 Mon Mar 23 21:26:18 2009
@@ -32,6 +32,8 @@
 import org.apache.shindig.gadgets.FakeGadgetSpecFactory;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.GadgetSpecFactory;
+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.oauth.AccessorInfo.OAuthParamLocation;
 import 
org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
@@ -46,6 +48,7 @@
 import net.oauth.OAuth.Parameter;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.ArrayUtils;
 import org.json.JSONObject;
 import org.junit.After;
 import org.junit.Before;
@@ -1026,6 +1029,65 @@
     byte[] echoedBytes = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(echoed));
     assertTrue(Arrays.equals(raw, echoedBytes));
   }
+  
+  @Test
+  public void testPostTamperedRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", 
"http://www.example.com/app";);
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("yo momma".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    try {
+      HttpResponse resp = 
client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL,
+          "funky-content", raw);
+      fail("Should have thrown with oauth_body_hash mismatch");
+    } catch (RuntimeException e) {
+      // good
+    }
+  }
+
+  @Test
+  public void testPostTamperedFormContent() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", 
"http://www.example.com/app";);
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("foo=quux".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    try {
+      HttpResponse resp = 
client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "foo=bar");
+      fail("Should have thrown with oauth signature mismatch");
+    } catch (RuntimeException e) {
+      // good
+    }
+  }
+  
+  @Test
+  public void testPostTamperedRemoveRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", 
"http://www.example.com/app";);
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody(ArrayUtils.EMPTY_BYTE_ARRAY);
+        request.setHeader("Content-Type", "application/x-www-form-urlencoded");
+        return serviceProvider.fetch(request);
+      }
+    });
+    try {
+      HttpResponse resp = 
client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL,
+          "funky-content", raw);
+      fail("Should have thrown with body hash in form encoded request");
+    } catch (RuntimeException e) {
+      // good
+    }
+  }
 
   @Test
   public void testSignedFetch_error401() throws Exception {


Reply via email to