Commits

pi songsiritat committed 8a344a0

OAUTH-273: made OAuth-family protocols all independent

  • Participants
  • Parent commits d4a9be7

Comments (0)

Files changed (13)

File api/src/main/java/com/atlassian/oauth/Consumer.java

     private final PublicKey publicKey;
     private final URI callback;
 
-    // 2 Legged OAuth parameters used by the service provider side.
+    private final boolean threeLOAllowed;
     private final boolean twoLOAllowed;
     private final String executingTwoLOUser;
     private final boolean twoLOImpersonationAllowed;
         publicKey = builder.publicKey;
         description = builder.description;
         callback = builder.callback;
+        threeLOAllowed = builder.threeLOAllowed;
         twoLOAllowed = builder.twoLOAllowed;
         executingTwoLOUser = builder.executingTwoLOUser;
         twoLOImpersonationAllowed = builder.twoLOImpersonationAllowed;
     }
 
     /**
+     * Returns whether 3 Legged OAuth requests are allowed for this consumer.
+     *
+     * @return whether 3 Legged OAuth requests are allowed for this consumer
+     */
+    public boolean getThreeLOAllowed()
+    {
+        return threeLOAllowed;
+    }
+
+    /**
      * Returns whether 2 Legged OAuth requests are allowed for this consumer.
      *
      * @return whether 2 Legged OAuth requests are allowed for this consumer
             .append("callback", callback)
             .append("signatureMethod", signatureMethod)
             .append("publicKey", publicKey)
+            .append("threeLOAllowed", threeLOAllowed)
             .append("twoLOAllowed", twoLOAllowed)
             .append("executingTwoLOUser", executingTwoLOUser)
             .append("twoLOImpersonationAllowed", twoLOImpersonationAllowed)
     /**
      * Builder allowing the optional attributes of the {@code Consumer} object under construction to be set and
      * construction of the final {@code Consumer} instance.
+     *
+     * By default a newly created Consumer will have 3LO enabled unless {@link #threeLOAllowed(false)} is called.
      */
     public static final class InstanceBuilder
     {
         private boolean twoLOAllowed;
         private String executingTwoLOUser;
         private boolean twoLOImpersonationAllowed;
+        private boolean threeLOAllowed;
 
         public InstanceBuilder(String key)
         {
             this.key = key;
+            // For API backward compatibility, not calling {@link #threeLOAllowed} would result in 3LO being allowed.
+            this.threeLOAllowed = true;
         }
 
         /**
         }
 
         /**
+         * Sets whether to allow 3LO requests from consumers
+         * {@code this} builder to allow other attributes to be set
+         *
+         * @param threeLOAllowed whether 3LO requests from the consumer are allowed
+         * @return {@code this} builder
+         */
+        public InstanceBuilder threeLOAllowed(boolean threeLOAllowed)
+        {
+            this.threeLOAllowed = threeLOAllowed;
+            return this;
+        }
+
+        /**
          * Sets whether to allow 2LO requests from consumers
          * {@code this} builder to allow other attributes to be set
          *

File api/src/test/java/com/atlassian/oauth/ConsumerBuilderTest.java

+package com.atlassian.oauth;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.URI;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+public class ConsumerBuilderTest
+{
+    private Consumer defaultConsumer;
+    private URI uri;
+
+    @Before
+    public void setUp()
+    {
+        URI uri = URI.create("http://www.atlassian.com");
+        defaultConsumer = Consumer.key("key").name("name")
+                            .description("description").callback(uri)
+                            .signatureMethod(Consumer.SignatureMethod.HMAC_SHA1)
+                            .build();
+    }
+
+    @Test
+    public void assertByDefaultConsumerBuilderCreates3LOEnabledConsumer()
+    {
+        assertTrue(defaultConsumer.getThreeLOAllowed());
+    }
+
+    @Test
+    public void assertByDefaultConsumerBuilderCreates2LODisabledConsumer()
+    {
+        assertFalse(defaultConsumer.getTwoLOAllowed());
+    }
+
+    @Test
+    public void assertByDefaultConsumerBuilderCreates2LOWithImpersonationDisabledConsumer()
+    {
+        assertFalse(defaultConsumer.getTwoLOImpersonationAllowed());
+    }
+
+    @Test
+    public void assertConsumerCanBeCreatedWith3LOOff()
+    {
+        Consumer consumer = Consumer.key("key").name("name")
+                                .description("description").callback(uri)
+                                .signatureMethod(Consumer.SignatureMethod.HMAC_SHA1)
+                                .threeLOAllowed(false)
+                                .build();
+        assertFalse(consumer.getThreeLOAllowed());
+    }
+}

File integration-tests/src/test/java/it/com/atlassian/oauth/OAuth2LOTest.java

 {
     private static final String CONSUMER_DOES_NOT_EXIST_KEY = "consumer_does_not_exist_key";
     private static final String CONSUMER_2LO_KEY = "hardcoded-2lo-consumer";
+    private static final String CONSUMER_2LO_ONLY_KEY = "hardcoded-2lo-only-consumer";
     private static final String CONSUMER_2LO_WITH_BAD_EXECUTING_USER_KEY = "hardcoded-2lo-consumer-bad-executing-user";
     private static final String CONSUMER_2LO_IMPERSONATION_KEY = "hardcoded-2lo-impersonation-consumer";
+    private static final String CONSUMER_2LO_IMPERSONATION_ONLY_KEY = "hardcoded-2lo-impersonation-only-consumer";
 
     @Test
     public void assertThatConsumerCannotAccessResourceIfTheConsumerKeyDoesNotExist() throws Exception
         }
         catch (OAuthProblemException e)
         {
-            assertThat(e.getProblem(), is(equalTo(PERMISSION_DENIED)));
+            assertThat(e.getProblem(), is(nullValue()));
         }
     }
 
             assertThat(e.getProblem(), is(nullValue()));
         }
     }
+
+    @Test
+    public void assertThat2LOImpersonationCanWorkCompletelyIndependentOf2LO() throws Exception
+    {
+        OAuthAccessor accessor = createAccessor(CONSUMER_2LO_IMPERSONATION_ONLY_KEY);
+        OAuthClient client = createClient();
+
+        // This is 2LO with Impersonation so it must work.
+        OAuthMessage response = client.invoke(accessor, STRICT_SECURITY_URL, ImmutableMap.of("oauth_token", "", "xoauth_requestor_id", "betty").entrySet());
+        assertThat(response.readBodyAsString(), is(equalTo("betty")));
+
+        // Now try standard 2LO, it must fail.
+        try
+        {
+            client.invoke(accessor, STRICT_SECURITY_URL, ImmutableMap.of("oauth_token", "").entrySet());
+            fail("OAuthProblemException expected");
+        }
+        catch (OAuthProblemException e)
+        {
+            assertThat(e.getProblem(), is(nullValue()));
+        }
+    }
+
+    @Test
+    public void assertThatConsumerCanAccessResourceThrough2LOOnlyConsumerMeaning2LOIsIndependentOf3LO() throws Exception
+    {
+        OAuthAccessor accessor = createAccessor(CONSUMER_2LO_ONLY_KEY);
+        OAuthClient client = createClient();
+
+        OAuthMessage response = client.invoke(accessor, STRICT_SECURITY_URL, ImmutableMap.of("oauth_token", "").entrySet());
+        assertThat(response.readBodyAsString(), is(equalTo("barney")));
+        assertThat(response.getHeader("X-OAUTH-REQUEST-ANNOTATED"), is(equalTo("true")));
+    }
 }

File integration-tests/src/test/java/it/com/atlassian/oauth/OAuthDanceTest.java

         assertThat(message.getParameter(OAUTH_CALLBACK_CONFIRMED), is(equalTo("true")));
     }
 
-    @Ignore
     @Test
     public void assertThatNonAuthenticatedUserGetsRedirectedToServiceProviderLogin() throws Exception
     {
     }
 
     @Test
+    public void assertThatConsumerCannotAccessRestrictedResourcesWithAccessTokenIf3LOForThatConsumerIsOff() throws Exception
+    {
+        OAuthAccessor accessor = createAccessor("hardcoded-2lo-only-consumer");
+        OAuthClient client = createClient();
+
+        accessor.accessToken = ACCESS_TOKEN_ON_NON_3LO_CONSUMER;
+        accessor.tokenSecret = ACCESS_TOKEN_SECRET_ON_NON_3LO_CONSUMER;
+
+        try
+        {
+            client.invoke(accessor, WHOAMI_URL, Collections.<Map.Entry<?, ?>>emptySet());
+        }
+        catch (OAuthProblemException ope)
+        {
+            assertThat(ope.getProblem(), is(equalTo(PERMISSION_DENIED)));
+        }
+    }
+
+    @Test
     public void assertThatConsumerCannotAccessRestrictedResourcesWithAccessTokenIfRequestConsumerKeyDoesNotMatchToken() throws Exception
     {
         OAuthAccessor accessor = createAccessor();

File integration-tests/src/test/java/it/com/atlassian/oauth/OAuthTestBase.java

     protected static final String ACCESS_TOKEN = "71b5607f60c0aae6161ce251dd55e8ed";
     protected static final String ACCESS_TOKEN_SECRET = "160881ffbe3c4ff0f6a1ba9078e92e83";
 
+    protected static final String ACCESS_TOKEN_ON_NON_3LO_CONSUMER = "91b5607f60c0aae6161ce251dd55e8ed";
+    protected static final String ACCESS_TOKEN_SECRET_ON_NON_3LO_CONSUMER = "960881ffbe3c4ff0f6a1ba9078e92e83";
+
     protected static final String NON_AUTHORIZED_V1_REQUEST_TOKEN_FOR_AUTHORIZING = "Quie9Tooico3ahShpagh0Voodoh1Phah";
     protected static final String NON_AUTHORIZED_V1_REQUEST_TOKEN_WITH_NO_CALLBACK_FOR_AUTHORIZING = "iqu3Ti8siet7uoShcoonaeT6zu5woh3H";
     protected static final String NON_AUTHORIZED_V1_REQUEST_TOKEN_FOR_DENYING = "eep2eeTaich5aiQuroos6IegIer2eife";

File service-provider-plugin/src/main/java/com/atlassian/oauth/serviceprovider/internal/AuthenticatorImpl.java

             }
             validate3LOMessage(message, token);
             consumer = validateConsumer(message);
+
+            if (!consumer.getThreeLOAllowed())
+            {
+                LOG.info("3-Legged OAuth request not allowed for Consumer key:'{}'" + consumer.getKey());
+                throw new OAuthProblemException(PERMISSION_DENIED);
+            }
         }
         catch (OAuthProblemException ope)
         {
         try
         {
             consumer = validateConsumer(message);
-
-            // 2LO must be specifically enabled to allow this operation.
-            if (!consumer.getTwoLOAllowed())
-            {
-                LOG.info("2LO request has been attempted but the 2LO feature is not enabled for consumer:'{}'.", consumer.getName());
-                throw new OAuthProblemException(PERMISSION_DENIED);
-            }
             validate2LOMessage(message, consumer);
         }
         catch (OAuthProblemException ope)
         else
         {
             // This is 2LO with no impersonation. We only resolve to the user assigned for execution.
+            if (!consumer.getTwoLOAllowed())
+            {
+                LOG.info("2LO request has been attempted but the 2LO feature is not enabled for consumer:'{}'.", consumer.getName());
+                sendError(response, HttpServletResponse.SC_UNAUTHORIZED, message);
+                return new Authenticator.Result.Failure(new OAuthProblem.PermissionDenied());
+            }
+
             // Let's make sure the user is valid.
             if (consumer.getExecutingTwoLOUser() == null)
             {

File service-provider-plugin/src/main/java/com/atlassian/oauth/serviceprovider/internal/OAuthProblem.java

         {
             super(Problem.PERMISSION_DENIED, username);
         }
+
+        // This constructor is used when the operation is not allowed that has nothing to do with the user.
+        public PermissionDenied()
+        {
+            super(Problem.PERMISSION_DENIED);
+        }
     }
 
     /**

File service-provider-plugin/src/main/java/com/atlassian/oauth/serviceprovider/internal/servlet/RequestTokenServlet.java

 import static net.oauth.OAuth.Problems.OAUTH_PARAMETERS_REJECTED;
 import static net.oauth.OAuth.Problems.OAUTH_PROBLEM_ADVICE;
 import static net.oauth.OAuth.Problems.PARAMETER_REJECTED;
+import static net.oauth.OAuth.Problems.PERMISSION_DENIED;
 import static net.oauth.OAuth.formEncode;
 import static net.oauth.server.OAuthServlet.handleException;
 
             {
                 throw new OAuthProblemException(CONSUMER_KEY_UNKNOWN);
             }
+            if (!consumer.getThreeLOAllowed())
+            {
+                throw new OAuthProblemException(PERMISSION_DENIED);
+            }
+
             URI callback;
             Version version;
             if (message.getParameter(OAUTH_CALLBACK) != null)

File service-provider-plugin/src/test/java/com/atlassian/oauth/serviceprovider/internal/servlet/RequestTokenServletTest.java

 import static com.atlassian.oauth.serviceprovider.ServiceProviderToken.Version.V_1_0;
 import static com.atlassian.oauth.serviceprovider.ServiceProviderToken.Version.V_1_0_A;
 import static com.atlassian.oauth.testing.TestData.Consumers.RSA_CONSUMER;
+import static com.atlassian.oauth.testing.TestData.Consumers.RSA_CONSUMER_WITH_2LO_ONLY;
 import static com.atlassian.oauth.testing.TestData.OAuthConsumers.RSA_OAUTH_CONSUMER;
 import static net.oauth.OAuth.OAUTH_CALLBACK;
 import static net.oauth.OAuth.OAUTH_CONSUMER_KEY;
         verify(response).setContentType(startsWith(OAuth.FORM_ENCODED));
         assertThat(responseStream.toString(), containsString("oauth_problem=parameter_rejected"));
     }
+
+    @Test
+    public void verifyThatRequestTokenRequestIsRejectedIfThreeLOIsOff() throws Exception
+    {
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getParameterMap()).thenReturn(ImmutableMap.of(
+                OAUTH_CONSUMER_KEY, new String[] { RSA_CONSUMER_WITH_2LO_ONLY.getKey() },
+                OAUTH_CALLBACK, new String[] { "http://consumer/callback" }
+        ));
+        when(consumerStore.get(RSA_CONSUMER_WITH_2LO_ONLY.getKey())).thenReturn(RSA_CONSUMER_WITH_2LO_ONLY);
+        when(tokenStore.put(UNAUTHORIZED_REQUEST_TOKEN)).thenReturn(UNAUTHORIZED_REQUEST_TOKEN);
+        when(factory.generateRequestToken(same(RSA_CONSUMER), eq(URI.create("http://consumer/callback")), isA(OAuthMessage.class), eq(V_1_0_A)))
+                .thenReturn(UNAUTHORIZED_REQUEST_TOKEN);
+
+        servlet.service(request, response);
+
+        assertThat(responseStream.toString(), containsString("oauth_problem=permission_denied"));
+    }
 }

File service-provider-sal-plugin/src/main/java/com/atlassian/oauth/serviceprovider/sal/PluginSettingsServiceProviderConsumerStore.java

                     .publicKey(props.getPublicKey())
                     .description(props.getDescription())
                     .callback(props.getCallback())
+                    .threeLOAllowed(props.getThreeLOAllowed())
                     .twoLOAllowed(props.getTwoLOAllowed())
                     .executingTwoLOUser(props.getExecutingTwoLOUser())
                     .twoLOImpersonationAllowed(props.getTwoLOImpersonationAllowed())
             static final String DESCRIPTION = "description";
             static final String NAME = "name";
 
+            static final String THREE_LO_ALLOWED = "threeLOAllowed";
             static final String TWO_LO_ALLOWED = "twoLOAllowed";
             static final String EXECUTING_TWO_LO_USER = "executingTwoLOUser";
             static final String TWO_LO_IMPERSONATION_ALLOWED = "twoLOImpersonationAllowed";
                 putPublicKey(consumer.getPublicKey());
                 putDescription(consumer.getDescription());
                 putCallback(consumer.getCallback());
+                putThreeLOAllowed(consumer.getThreeLOAllowed());
                 putTwoLOAllowed(consumer.getTwoLOAllowed());
                 putExecutingTwoLOUser(consumer.getExecutingTwoLOUser());
                 putTwoLOImpersonationAllowed(consumer.getTwoLOImpersonationAllowed());
                 put(CALLBACK, callback.toString());
             }
 
+            boolean getThreeLOAllowed()
+            {
+                String isAllowed = get(THREE_LO_ALLOWED);
+
+                // for backward compatibility, not having this defined means it's on.
+                if (isAllowed == null)
+                {
+                    return true;
+                }
+
+                return Boolean.parseBoolean(isAllowed);
+            }
+
+            void putThreeLOAllowed(boolean threeLOAllowed)
+            {
+                put(THREE_LO_ALLOWED, Boolean.toString(threeLOAllowed));
+            }
+
             boolean getTwoLOAllowed()
             {
                 String isAllowed = get(TWO_LO_ALLOWED);

File service-provider-sal-plugin/src/test/java/com/atlassian/oauth/serviceprovider/sal/PluginSettingsServiceProviderConsumerStoreTest.java

     public void assertThatWhatIsStoredIsActuallyReturnedAsIs()
     {
         Consumer consumer1 = Consumer.key("consumer1").callback(URI.create("http://other/callback")).description("other description").name("other consumer")
-                 .twoLOAllowed(true).executingTwoLOUser("user1").twoLOImpersonationAllowed(true).publicKey(KEYS.getPublic()).build();
+                 .threeLOAllowed(false).twoLOAllowed(true).executingTwoLOUser("user1").twoLOImpersonationAllowed(true).publicKey(KEYS.getPublic()).build();
 
         store.put(consumer1);
 
     }
 
     @Test
+    public void assertThat3LOFlagMissingMeansItsEnabled()
+    {
+        Properties props = new Properties();
+        props.put("name", "name1");
+        props.put("description", "description1");
+        props.put("callback", "callback1");
+        props.put("publicKey", RSAKeys.toPemEncoding(RSA_CONSUMER.getPublicKey()));
+
+        settings.put(ServiceProviderConsumerStore.class.getName() + ".consumer." + RSA_CONSUMER.getKey(), props);
+        settings.put(ServiceProviderConsumerStore.class.getName() + ".allConsumerKeys", "consumer-rsa");
+
+        Consumer consumer = store.get(RSA_CONSUMER.getKey());
+        assertThat(consumer.getThreeLOAllowed(), is(true));
+    }
+
+    @Test
     public void assertThatConsumerPropertyKeysAreLessThanOneHundredCharacters()
     {
         store.put(RSA_CONSUMER_WITH_LONG_KEY);
         properties.put("description", consumer.getDescription());
         properties.put("callback", consumer.getCallback().toString());
         properties.put("publicKey", RSAKeys.toPemEncoding(consumer.getPublicKey()));
+        properties.put("threeLOAllowed", Boolean.toString(consumer.getThreeLOAllowed()));
         properties.put("twoLOAllowed", Boolean.toString(consumer.getTwoLOAllowed()));
         if (consumer.getExecutingTwoLOUser() != null)
         {

File service-provider-testdata-plugin/src/main/java/com/atlassian/oauth/serviceprovider/testdata/HardcodedConsumerAndTokenSetup.java

                 .build();
         consumerStore.put(hardcoded2LOConsumer);
 
+        Consumer hardcoded2LOOnlyConsumer = Consumer.key("hardcoded-2lo-only-consumer")
+                .name("Hardcoded 2LO Only Consumer")
+                .publicKey(publicKey)
+                .description("Hardcoded 2LO Consumer")
+                .threeLOAllowed(false)
+                .twoLOAllowed(true)
+                .executingTwoLOUser("barney")
+                .build();
+        consumerStore.put(hardcoded2LOOnlyConsumer);
+
         Consumer hardcoded2LOConsumerBadExecutingUser = Consumer.key("hardcoded-2lo-consumer-bad-executing-user")
                 .name("Hardcoded 2LO Consumer with bad executing user")
                 .publicKey(publicKey)
                 .build();
         consumerStore.put(hardcoded2LOImpersonationConsumer);
 
+        Consumer hardcoded2LOImpersonationOnlyConsumer = Consumer.key("hardcoded-2lo-impersonation-only-consumer")
+                .name("Hardcoded 2LO with Impersonation Only Consumer")
+                .publicKey(publicKey)
+                .description("Hardcoded 2LO with Impersonation Only Consumer")
+                .threeLOAllowed(false)
+                .twoLOAllowed(false)
+                .executingTwoLOUser("barney")
+                .twoLOImpersonationAllowed(true)
+                .build();
+        consumerStore.put(hardcoded2LOImpersonationOnlyConsumer);
+
         URI callback = URI.create("http://consumer/callback");
 
         ServiceProviderToken nonAuthorizedRequestTokenForAuthorizing = ServiceProviderToken.newRequestToken("bb6dd1391ce33b5bd3ecad1175139a39")
             .build();
         store.put(accessToken);
 
+        ServiceProviderToken accessTokenOnNon3LOConsumer = ServiceProviderToken.newAccessToken("91b5607f60c0aae6161ce251dd55e8ed")
+                .tokenSecret("960881ffbe3c4ff0f6a1ba9078e92e83")
+                .consumer(hardcoded2LOOnlyConsumer)
+                .authorizedBy(userManager.resolve("fred"))
+                .properties(new HashMap<String, String>() {{ put("alternate.consumer.name", "TooCool Gadget"); }})
+                .build();
+        store.put(accessTokenOnNon3LOConsumer);
+
         // OAuth v1 test tokens
         ServiceProviderToken nonAuthorizedVersion1RequestTokenForAuthorizing = ServiceProviderToken.newRequestToken("Quie9Tooico3ahShpagh0Voodoh1Phah")
             .tokenSecret("29c3005cc5fbe5d431f27b29d6191ea3")
             .session(newSession("Ohs5ux1kzohJu4Eeaiv0no3Ujoowae8F").creationTime(0).lastRenewalTime(100).build())
             .build();
         store.put(nonRenewableAccessToken);
+
+        ServiceProviderToken nonAuthorizedRequestTokenForAuthorizingOnDisallowedConsumer = ServiceProviderToken.newRequestToken("996dd1391ce33b5bd3ecad1175139a39")
+                .tokenSecret("29c3005cc5fbe5d431f27b29d6191e99")
+                .consumer(hardcoded2LOImpersonationOnlyConsumer)
+                .callback(callback)
+                .version(Version.V_1_0_A)
+                .build();
+        store.put(nonAuthorizedRequestTokenForAuthorizingOnDisallowedConsumer);
     }
 }

File test-utils/src/main/java/com/atlassian/oauth/testing/TestData.java

                 .executingTwoLOUser("bob")
                 .build();
 
+        public static final Consumer RSA_CONSUMER_WITH_2LO_ONLY = Consumer.key("consumer-rsa-with-2lo-only")
+                .name("Consumer using RSA")
+                .description("description")
+                .signatureMethod(SignatureMethod.RSA_SHA1)
+                .publicKey(KEYS.getPublic())
+                .callback(URI.create("http://consumer/callback"))
+                .threeLOAllowed(false)
+                .twoLOAllowed(true)
+                .executingTwoLOUser("bob")
+                .build();
+
         public static final Consumer RSA_CONSUMER_WITH_2LO_BUT_NO_EXECUTING_USER = Consumer.key("consumer-rsa-with-2lo-but-no-executing-user")
                 .name("Consumer using RSA")
                 .description("description")