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 {