You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by be...@apache.org on 2009/11/13 18:08:51 UTC

svn commit: r835923 - in /incubator/shindig/trunk/java: common/src/main/java/org/apache/shindig/auth/ gadgets/src/main/java/org/apache/shindig/gadgets/oauth/ gadgets/src/main/java/org/apache/shindig/gadgets/oauth/testing/ gadgets/src/test/java/org/apac...

Author: beaton
Date: Fri Nov 13 17:08:50 2009
New Revision: 835923

URL: http://svn.apache.org/viewvc?rev=835923&view=rev
Log:
So it turns out that Twitter returns 403 responses when a client is rate
limited.  This causes the oauth proxy to delete oauth tokens.  Not ideal, since
the token is still valid.

I'm modifying our code so that tokens are only dropped when we get a 401
response.

This looks safe.  I've tested several OAuth service providers, and they all
return 401 on invalid tokens.


Modified:
    incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
    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/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/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java?rev=835923&r1=835922&r2=835923&view=diff
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java (original)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java Fri Nov 13 17:08:50 2009
@@ -25,4 +25,25 @@
   public static final String OAUTH_BODY_HASH = "oauth_body_hash";
   public static final String OAUTH_VERIFIER = "oauth_verifier";
   public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
-}
+  
+  public static final String PROBLEM_ACCESS_TOKEN_EXPIRED = "access_token_expired";
+
+  public static final String PROBLEM_PARAMETER_MISSING = "parameter_missing";
+
+  public static final String PROBLEM_TOKEN_REVOKED = "token_revoked";
+
+  public static final String PROBLEM_TOKEN_INVALID = "token_invalid";
+
+  public static final String PROBLEM_PARAMETER_ABSENT = "parameter_absent";
+
+  public static final String PROBLEM_BAD_VERIFIER = "bad_verifier";
+
+  public static final String PROBLEM_TOKEN_REJECTED = "token_rejected";
+
+  public static final String PROBLEM_PARAMETER_REJECTED = "parameter_rejected";
+
+  public static final String PROBLEM_PERMISSION_DENIED = "permission_denied";
+
+  public static final String PROBLEM_CONSUMER_KEY_REFUSED = "consumer_key_refused";
+
+  public static final String PROBLEM_CONSUMER_KEY_UNKNOWN = "consumer_key_unknown";}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java?rev=835923&r1=835922&r2=835923&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java Fri Nov 13 17:08:50 2009
@@ -77,6 +77,7 @@
     final String consumerSecret = params.get("--consumerSecret");
     final String xOauthRequestor = params.get("--requestorId");
     final String accessToken = params.get("--accessToken");
+    final String tokenSecret = params.get("--tokenSecret");
     final String method = params.get("--method") == null ? "GET" :params.get("--method");
     String url = params.get("--url");
     String contentType = params.get("--contentType");
@@ -134,6 +135,7 @@
     OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
     OAuthAccessor accessor = new OAuthAccessor(consumer);
     accessor.accessToken = accessToken;
+    accessor.tokenSecret = tokenSecret;
     OAuthMessage message = accessor.newRequestMessage(method, target.toString(), oauthParams);
 
     List<Map.Entry<String, String>> entryList = OAuthRequest.selectOAuthParams(message);

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java?rev=835923&r1=835922&r2=835923&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java Fri Nov 13 17:08:50 2009
@@ -18,6 +18,7 @@
 package org.apache.shindig.gadgets.oauth;
 
 import org.apache.shindig.auth.OAuthUtil;
+import org.apache.shindig.gadgets.http.HttpResponse;
 
 import com.google.common.collect.ImmutableSet;
 
@@ -79,7 +80,7 @@
 
   private final String problemCode;
 
-  public OAuthProtocolException(OAuthMessage reply) {
+  public OAuthProtocolException(int status, OAuthMessage reply) {
     String problem = OAuthUtil.getParameter(reply, OAuthProblemException.OAUTH_PROBLEM);
     if (problem == null) {
       throw new IllegalArgumentException("No problem reported for OAuthProtocolException");
@@ -98,8 +99,14 @@
       canRetry = true;
       canExtend = true;
     } else {
-      startFromScratch = true;
-      canRetry = true;
+      // fallback to status to figure out behavior
+      if (status == HttpResponse.SC_UNAUTHORIZED) {
+        startFromScratch = true;
+        canRetry = true;
+      } else {
+        startFromScratch = false;
+        canRetry = false;
+      }
       canExtend = false;
     }
   }
@@ -110,11 +117,11 @@
    * @param status HTTP status code, assumed to be between 400 and 499 inclusive
    */
   public OAuthProtocolException(int status) {
-    if (status == 401) {
+    if (status == HttpResponse.SC_UNAUTHORIZED) {
       startFromScratch = true;
       canRetry = true;
     } else {
-      startFromScratch = true;
+      startFromScratch = false;
       canRetry = false;
     }
     canExtend = false;

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=835923&r1=835922&r2=835923&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 Fri Nov 13 17:08:50 2009
@@ -842,14 +842,17 @@
    * @throws OAuthProtocolException
    */
   private void checkForProtocolProblem(HttpResponse response) throws OAuthProtocolException {
-    if (isFullOAuthError(response)) {
+    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(message);
+        throw new OAuthProtocolException(response.getHttpStatusCode(), message);
       }
       // No extended information, guess based on HTTP response code.
-      throw new OAuthProtocolException(response.getHttpStatusCode());
+      if (response.getHttpStatusCode() == HttpResponse.SC_UNAUTHORIZED) {
+        throw new OAuthProtocolException(response.getHttpStatusCode());
+      }
     }
   }
 
@@ -858,9 +861,12 @@
    * errors for signed fetch, we only care about places where we are dealing with OAuth request
    * and/or access tokens.
    */
-  private boolean isFullOAuthError(HttpResponse response) {
-    // 401 and 403 are likely to be authentication errors.
-    if (response.getHttpStatusCode() != HttpResponse.SC_UNAUTHORIZED
+  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;
     }

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=835923&r1=835922&r2=835923&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 Fri Nov 13 17:08:50 2009
@@ -59,6 +59,8 @@
 
 public class FakeOAuthServiceProvider implements HttpFetcher {
 
+
+
   public static final String BODY_ECHO_HEADER = "X-Echoed-Body";
 
   public static final String RAW_BODY_ECHO_HEADER = "X-Echoed-Raw-Body";
@@ -182,6 +184,7 @@
   private final OAuthConsumer oauthConsumer;
   private final TimeSource clock;
 
+  private boolean unauthorized = false;
   private boolean throttled = false;
   private boolean vagueErrors = false;
   private boolean reportExpirationTimes = true;
@@ -301,16 +304,24 @@
       consumer = oauthConsumer;
     } else {
       return makeOAuthProblemReport(
-          "consumer_key_unknown", "invalid consumer: " + requestConsumer);
+          OAuthConstants.PROBLEM_CONSUMER_KEY_UNKNOWN, "invalid consumer: " + requestConsumer,
+          HttpResponse.SC_FORBIDDEN);
     }
     if (throttled) {
       return makeOAuthProblemReport(
-          "consumer_key_refused", "exceeded quota exhausted");
+          OAuthConstants.PROBLEM_CONSUMER_KEY_REFUSED, "exceeded quota exhausted",
+          HttpResponse.SC_FORBIDDEN);
+    }
+    if (unauthorized) {
+      return makeOAuthProblemReport(
+          OAuthConstants.PROBLEM_PERMISSION_DENIED, "user refused access",
+          HttpResponse.SC_BAD_REQUEST);
     }
     if (rejectExtraParams) {
       String extra = hasExtraParams(info.message);
       if (extra != null) {
-        return makeOAuthProblemReport("parameter_rejected", extra);
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_PARAMETER_REJECTED, extra,
+            HttpResponse.SC_BAD_REQUEST);
       }
     }
     OAuthAccessor accessor = new OAuthAccessor(consumer);
@@ -339,12 +350,8 @@
     return null;
   }
 
-  private HttpResponse makeOAuthProblemReport(String code, String text) throws IOException {
+  private HttpResponse makeOAuthProblemReport(String code, String text, int rc) throws IOException {
     if (vagueErrors) {
-      int rc = HttpResponse.SC_UNAUTHORIZED;
-      if ("consumer_key_unknown".equals(code)) {
-        rc = HttpResponse.SC_FORBIDDEN;
-      }
       return new HttpResponseBuilder()
           .setHttpStatusCode(rc)
           .setResponseString("some vague error")
@@ -354,7 +361,7 @@
     msg.addParameter("oauth_problem", code);
     msg.addParameter("oauth_problem_advice", text);    
     return new HttpResponseBuilder()
-        .setHttpStatusCode(HttpResponse.SC_FORBIDDEN)
+        .setHttpStatusCode(rc)
         .addHeader("WWW-Authenticate", msg.getAuthorizationHeader("realm"))
         .create();
   }
@@ -560,15 +567,20 @@
     String requestToken = info.message.getParameter("oauth_token");
     TokenState state = tokenState.get(requestToken);
     if (throttled) {
-      return makeOAuthProblemReport(
-          "consumer_key_refused", "exceeded quota");
+      return makeOAuthProblemReport(OAuthConstants.PROBLEM_CONSUMER_KEY_REFUSED,
+          "exceeded quota", HttpResponse.SC_FORBIDDEN);
+    } else if (unauthorized) {
+      return makeOAuthProblemReport(OAuthConstants.PROBLEM_PERMISSION_DENIED,
+          "user refused access", HttpResponse.SC_UNAUTHORIZED);
     } else if (state == null) {
-      return makeOAuthProblemReport("token_rejected", "Unknown request token");
+      return makeOAuthProblemReport(OAuthConstants.PROBLEM_TOKEN_REJECTED,
+          "Unknown request token", HttpResponse.SC_UNAUTHORIZED);
     }   
     if (rejectExtraParams) {
       String extra = hasExtraParams(info.message);
       if (extra != null) {
-        return makeOAuthProblemReport("parameter_rejected", extra);
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_PARAMETER_REJECTED,
+            extra, HttpResponse.SC_BAD_REQUEST);
       }
     }
 
@@ -580,21 +592,25 @@
     if (state.getState() == State.APPROVED_UNCLAIMED) {
       String sentVerifier = info.message.getParameter("oauth_verifier");
       if (state.verifier != null && !state.verifier.equals(sentVerifier)) {
-        return makeOAuthProblemReport("bad_verifier", "wrong oauth verifier");
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_BAD_VERIFIER, "wrong oauth verifier",
+            HttpResponse.SC_UNAUTHORIZED);
       }
       state.claimToken();
     } else if (state.getState() == State.APPROVED) {
       // Verify can refresh
       String sentHandle = info.message.getParameter("oauth_session_handle");
       if (sentHandle == null) {
-        return makeOAuthProblemReport("parameter_absent", "no oauth_session_handle");
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_PARAMETER_ABSENT,
+            "no oauth_session_handle", HttpResponse.SC_BAD_REQUEST);
       }
       if (!sentHandle.equals(state.sessionHandle)) {
-        return makeOAuthProblemReport("token_invalid", "token not valid");
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_TOKEN_INVALID, "token not valid",
+            HttpResponse.SC_UNAUTHORIZED);
       }
       state.renewToken();
     } else if (state.getState() == State.REVOKED){
-      return makeOAuthProblemReport("token_revoked", "Revoked access token can't be renewed");
+      return makeOAuthProblemReport(OAuthConstants.PROBLEM_TOKEN_REVOKED,
+          "Revoked access token can't be renewed", HttpResponse.SC_UNAUTHORIZED);
     } else {
       throw new Exception("Token in weird state " + state.getState());
     }
@@ -633,13 +649,19 @@
     } else if ("container.com".equals(consumerId)) {
       consumer = signedFetchConsumer;
     } else {
-      return makeOAuthProblemReport("parameter_missing", "oauth_consumer_key not found");
+      return makeOAuthProblemReport(OAuthConstants.PROBLEM_PARAMETER_MISSING,
+          "oauth_consumer_key not found", HttpResponse.SC_BAD_REQUEST);
     }
     OAuthAccessor accessor = new OAuthAccessor(consumer);
     String responseBody = null;
     if (throttled) {
       return makeOAuthProblemReport(
-          "consumer_key_refused", "exceeded quota");
+          OAuthConstants.PROBLEM_CONSUMER_KEY_REFUSED, "exceeded quota", HttpResponse.SC_FORBIDDEN);
+    }
+    if (unauthorized) {
+      return makeOAuthProblemReport(
+          OAuthConstants.PROBLEM_PERMISSION_DENIED, "user refused access",
+          HttpResponse.SC_UNAUTHORIZED);
     }
     if (consumer == oauthConsumer) {
       // for OAuth, check the access token.  We skip this for signed fetch
@@ -647,7 +669,8 @@
       TokenState state = tokenState.get(accessToken);
       if (state == null) {
         return makeOAuthProblemReport(
-            "token_rejected", "Access token unknown");
+            OAuthConstants.PROBLEM_TOKEN_REJECTED, "Access token unknown",
+            HttpResponse.SC_UNAUTHORIZED);
       }
       // Check the signature
       accessor.accessToken = accessToken;
@@ -656,12 +679,14 @@
 
       if (state.getState() != State.APPROVED) {
         return makeOAuthProblemReport(
-            "token_revoked", "User revoked permissions");
+            OAuthConstants.PROBLEM_TOKEN_REVOKED, "User revoked permissions",
+            HttpResponse.SC_UNAUTHORIZED);
       }
       if (sessionExtension) {
         long expiration = state.issued + TOKEN_EXPIRATION_SECONDS * 1000;
         if (expiration < clock.currentTimeMillis()) {
-          return makeOAuthProblemReport("access_token_expired", "token needs to be refreshed");
+          return makeOAuthProblemReport(OAuthConstants.PROBLEM_ACCESS_TOKEN_EXPIRED,
+              "token needs to be refreshed", HttpResponse.SC_UNAUTHORIZED);
         }
       }
       responseBody = "User data is " + state.getUserData();
@@ -747,6 +772,10 @@
   public void setConsumersThrottled(boolean throttled) {
     this.throttled = throttled;
   }
+  
+  public void setConsumerUnauthorized(boolean unauthorized) {
+    this.unauthorized = unauthorized;
+  }
 
   public void setReturnNull(boolean returnNull) {
     this.returnNull = returnNull;

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=835923&r1=835922&r2=835923&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 Fri Nov 13 17:08:50 2009
@@ -237,4 +237,8 @@
   public void setReceivedCallbackUrl(String receivedCallbackUrl) {
     this.receivedCallbackUrl = receivedCallbackUrl;
   }
+  
+  public void clearState() {
+    this.oauthState = null;
+  }
 }

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=835923&r1=835922&r2=835923&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 Fri Nov 13 17:08:50 2009
@@ -755,7 +755,9 @@
 
     response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
     assertEquals("", response.getResponseAsString());
-    assertNotNull(response.getMetadata().get("oauthApprovalUrl"));
+    assertEquals(HttpResponse.SC_FORBIDDEN, response.getHttpStatusCode());
+    assertEquals("parameter_missing", response.getMetadata().get("oauthError"));
+    assertNull(response.getMetadata().get("oauthApprovalUrl"));
   }
 
   @Test
@@ -781,7 +783,7 @@
     checkStringContains("should return original request", errorText, "GET /data?cachebust=2\n");
     checkStringContains("should return signed request", errorText, "GET /data?cachebust=2&");
     checkStringContains("should remove secret", errorText, "oauth_token_secret=REMOVED");
-    checkStringContains("should return response", errorText, "HTTP/1.1 403");
+    checkStringContains("should return response", errorText, "HTTP/1.1 401");
     checkStringContains("should return response", errorText, "oauth_problem=\"token_revoked\"");
 
     client.approveToken("user_data=reapproved");
@@ -1004,6 +1006,66 @@
     assertEquals(3, serviceProvider.getResourceAccessCount());
 
     serviceProvider.setConsumersThrottled(false);
+    client.clearState();
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(4, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testConsumerThrottled_vagueErrors() throws Exception {
+    serviceProvider.setVagueErrors(true);
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=hello-oauth");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(true);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals("some vague error", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals(null, metadata.get("oauthError"));
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2\n");
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2&oauth_body_hash=2jm");
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(false);
+    
+    client.clearState(); // remove any cached oauth tokens
 
     response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
     assertEquals("User data is hello-oauth", response.getResponseAsString());
@@ -1333,7 +1395,7 @@
   @Test
   public void testSignedFetch_error401() throws Exception {
     assertEquals(0, base.getAccessTokenRemoveCount());
-    serviceProvider.setConsumersThrottled(true);
+    serviceProvider.setConsumerUnauthorized(true);
     serviceProvider.setVagueErrors(true);
     MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
 
@@ -1345,6 +1407,22 @@
     checkStringContains("Should return response", errorText, "some vague error");
     assertEquals(0, base.getAccessTokenRemoveCount());
   }
+  
+  @Test
+  public void testSignedFetch_error403() throws Exception {
+    assertEquals(0, base.getAccessTokenRemoveCount());
+    serviceProvider.setConsumersThrottled(true);
+    serviceProvider.setVagueErrors(true);
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertNull(response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("Should return sent request", errorText, "GET /data");
+    checkStringContains("Should return response", errorText, "HTTP/1.1 403");
+    checkStringContains("Should return response", errorText, "some vague error");
+    assertEquals(0, base.getAccessTokenRemoveCount());
+  }
 
   @Test
   public void testSignedFetch_unnamedConsumerKey() throws Exception {