keycloak-aplcache

feat: added PayPal IDP (#4449)

9/12/2017 6:57:59 AM

Changes

Details

diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
new file mode 100644
index 0000000..a3f4602
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.social.paypal;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Petter Lysne (petterlysne at hotmail dot com)
+ */
+public class PayPalIdentityProvider extends AbstractOAuth2IdentityProvider<PayPalIdentityProviderConfig> implements SocialIdentityProvider<PayPalIdentityProviderConfig>{
+
+  public static final String BASE_URL = "https://api.paypal.com/v1";
+  public static final String AUTH_URL = "https://www.paypal.com/signin/authorize";
+	public static final String TOKEN_RESOURCE = "/identity/openidconnect/tokenservice";
+	public static final String PROFILE_RESOURCE = "/oauth2/token/userinfo?schema=openid";
+	public static final String DEFAULT_SCOPE = "openid profile email";
+
+	public PayPalIdentityProvider(KeycloakSession session, PayPalIdentityProviderConfig config) {
+		super(session, config);
+		config.setAuthorizationUrl(config.targetSandbox() ? "https://www.sandbox.paypal.com/signin/authorize" : AUTH_URL);
+		config.setTokenUrl((config.targetSandbox() ? "https://api.sandbox.paypal.com/v1" : BASE_URL) + TOKEN_RESOURCE);
+		config.setUserInfoUrl((config.targetSandbox() ? "https://api.sandbox.paypal.com/v1" : BASE_URL) + PROFILE_RESOURCE);
+	}
+
+	@Override
+	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+		try {
+			JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
+
+			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
+
+			user.setUsername(getJsonProperty(profile, "email"));
+			user.setName(getJsonProperty(profile, "name"));
+			user.setEmail(getJsonProperty(profile, "email"));
+			user.setIdpConfig(getConfig());
+			user.setIdp(this);
+
+			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+			return user;
+		} catch (Exception e) {
+			throw new IdentityBrokerException("Could not obtain user profile from paypal.", e);
+		}
+	}
+
+	@Override
+	protected String getDefaultScopes() {
+		return DEFAULT_SCOPE;
+	}
+}
diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderConfig.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderConfig.java
new file mode 100644
index 0000000..dc94e3a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderConfig.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.social.paypal;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.models.IdentityProviderModel;
+
+/**
+ * @author Petter Lysne (petterlysne at hotmail dot com)
+ */
+public class PayPalIdentityProviderConfig extends OAuth2IdentityProviderConfig {
+
+    public PayPalIdentityProviderConfig(IdentityProviderModel model) {
+        super(model);
+    }
+
+    public boolean targetSandbox() {
+        String sandbox = getConfig().get("sandbox");
+        return sandbox == null ? false : Boolean.valueOf(sandbox);
+    }
+
+    public void setSandbox(boolean sandbox) {
+        getConfig().put("sandbox", String.valueOf(sandbox));
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderFactory.java
new file mode 100644
index 0000000..b0b5541
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProviderFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.social.paypal;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Petter Lysne
+ */
+public class PayPalIdentityProviderFactory extends AbstractIdentityProviderFactory<PayPalIdentityProvider> implements SocialIdentityProviderFactory<PayPalIdentityProvider> {
+
+    public static final String PROVIDER_ID = "paypal";
+
+    @Override
+    public String getName() {
+        return "PayPal";
+    }
+
+    @Override
+    public PayPalIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
+        return new PayPalIdentityProvider(session, new PayPalIdentityProviderConfig(model));
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalUserAttributeMapper.java b/services/src/main/java/org/keycloak/social/paypal/PayPalUserAttributeMapper.java
new file mode 100644
index 0000000..8311cda
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalUserAttributeMapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.social.paypal;
+
+import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+
+/**
+ * User attribute mapper.
+ * 
+ * @author Petter Lysne (petterlysne at hotmail dot com)
+ */
+public class PayPalUserAttributeMapper extends AbstractJsonUserAttributeMapper {
+
+	private static final String[] cp = new String[] { PayPalIdentityProviderFactory.PROVIDER_ID };
+
+	@Override
+	public String[] getCompatibleProviders() {
+		return cp;
+	}
+
+	@Override
+	public String getId() {
+		return "paypal-user-attribute-mapper";
+	}
+
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
index 0c91c4f..d907571 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
@@ -27,6 +27,7 @@ org.keycloak.broker.saml.mappers.UserAttributeMapper
 org.keycloak.broker.saml.mappers.UsernameTemplateMapper
 org.keycloak.social.facebook.FacebookUserAttributeMapper
 org.keycloak.social.github.GitHubUserAttributeMapper
+org.keycloak.social.paypal.PayPalUserAttributeMapper
 org.keycloak.social.google.GoogleUserAttributeMapper
 org.keycloak.social.linkedin.LinkedInUserAttributeMapper
 org.keycloak.social.stackoverflow.StackoverflowUserAttributeMapper
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
index f2861ab..db57e6b 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
@@ -16,6 +16,7 @@
 #
 
 org.keycloak.social.facebook.FacebookIdentityProviderFactory
+org.keycloak.social.paypal.PayPalIdentityProviderFactory
 org.keycloak.social.github.GitHubIdentityProviderFactory
 org.keycloak.social.google.GoogleIdentityProviderFactory
 org.keycloak.social.linkedin.LinkedInIdentityProviderFactory
@@ -24,4 +25,4 @@ org.keycloak.social.twitter.TwitterIdentityProviderFactory
 org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
 org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
 org.keycloak.social.gitlab.GitLabIdentityProviderFactory
-org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory
\ No newline at end of file
+org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory
diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 6c1a07c..4033559 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -294,7 +294,7 @@ The Welcome Page tests need to be run on WildFly/EAP and with `-Dskip.add.user.j
 The social login tests require setup of all social networks including an example social user. These details can't be 
 shared as it would result in the clients and users eventually being blocked. By default these tests are skipped.
    
-To run the full test you need to configure clients in Google, Facebook, GitHub, Twitter, LinkedIn, Microsoft and 
+To run the full test you need to configure clients in Google, Facebook, GitHub, Twitter, LinkedIn, Microsoft, PayPal and 
 StackOverflow. See the server administration guide for details on how to do that. Further, you also need to create a 
 sample user that can login to the social network.
  
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/PayPalLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/PayPalLoginPage.java
new file mode 100644
index 0000000..4f98213
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/PayPalLoginPage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.pages.social;
+
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author Petter Lysne (petterlysne at hotmail dot com)
+ */
+public class PayPalLoginPage extends AbstractSocialLoginPage {
+    @FindBy(id = "email")
+    private WebElement usernameInput;
+
+    @FindBy(id = "password")
+    private WebElement passwordInput;
+
+    @FindBy(name = "btnLogin")
+    private WebElement loginButton;
+
+    @FindBy(name = "continueLogin")
+    private WebElement continueLoginButton;
+
+    @Override
+    public void login(String user, String password) {
+        try {
+            usernameInput.clear(); // to remove pre-filled email
+            usernameInput.sendKeys(user);
+            passwordInput.sendKeys(password);
+            loginButton.click();
+        }
+        catch (NoSuchElementException e) {
+            continueLoginButton.click(); // already logged in, just need to confirm it
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
index 547d039..e34dd78 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
@@ -20,6 +20,7 @@ import org.keycloak.testsuite.pages.social.GitHubLoginPage;
 import org.keycloak.testsuite.pages.social.GoogleLoginPage;
 import org.keycloak.testsuite.pages.social.LinkedInLoginPage;
 import org.keycloak.testsuite.pages.social.MicrosoftLoginPage;
+import org.keycloak.testsuite.pages.social.PayPalLoginPage;
 import org.keycloak.testsuite.pages.social.StackOverflowLoginPage;
 import org.keycloak.testsuite.pages.social.TwitterLoginPage;
 import org.keycloak.testsuite.util.IdentityProviderBuilder;
@@ -42,6 +43,7 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.PAYPAL;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.STACKOVERFLOW;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.TWITTER;
@@ -70,6 +72,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         TWITTER("twitter", TwitterLoginPage.class),
         LINKEDIN("linkedin", LinkedInLoginPage.class),
         MICROSOFT("microsoft", MicrosoftLoginPage.class),
+        PAYPAL("paypal", PayPalLoginPage.class),
         STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
         OPENSHIFT("openshift-v3", null);
 
@@ -191,6 +194,13 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     @Test
+    public void paypalLogin() {
+        currentTestProvider = PAYPAL;
+        performLogin();
+        assertAccount();
+    }
+
+    @Test
     public void stackoverflowLogin() {
         currentTestProvider = STACKOVERFLOW;
         performLogin();
@@ -209,6 +219,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         if (provider == OPENSHIFT) {
             idp.getConfig().put("baseUrl", config.getProperty(provider.id() + ".baseUrl", OpenshiftV3IdentityProvider.BASE_URL));
         }
+        if (provider == PAYPAL) {
+            idp.getConfig().put("sandbox", getConfig(provider, "sandbox"));
+        }
         return idp;
     }
 
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
index cebe067..d84c80a 100644
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
@@ -21,6 +21,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
 import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
 import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
 import org.keycloak.social.github.GitHubIdentityProviderFactory;
+import org.keycloak.social.paypal.PayPalIdentityProviderFactory;
 import org.keycloak.social.google.GoogleIdentityProviderFactory;
 import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
 import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
@@ -47,6 +48,7 @@ public abstract class AbstractIdentityProviderModelTest extends AbstractModelTes
         this.expectedProviders.add(GoogleIdentityProviderFactory.PROVIDER_ID);
         this.expectedProviders.add(FacebookIdentityProviderFactory.PROVIDER_ID);
         this.expectedProviders.add(GitHubIdentityProviderFactory.PROVIDER_ID);
+        this.expectedProviders.add(PayPalIdentityProviderFactory.PROVIDER_ID);
         this.expectedProviders.add(TwitterIdentityProviderFactory.PROVIDER_ID);
         this.expectedProviders.add(LinkedInIdentityProviderFactory.PROVIDER_ID);
         this.expectedProviders.add(StackoverflowIdentityProviderFactory.PROVIDER_ID);
diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index 8d70f07..fcacc36 100755
--- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -33,6 +33,9 @@ import org.keycloak.social.facebook.FacebookIdentityProvider;
 import org.keycloak.social.facebook.FacebookIdentityProviderFactory;
 import org.keycloak.social.github.GitHubIdentityProvider;
 import org.keycloak.social.github.GitHubIdentityProviderFactory;
+import org.keycloak.social.paypal.PayPalIdentityProvider;
+import org.keycloak.social.paypal.PayPalIdentityProviderFactory;
+import org.keycloak.social.paypal.PayPalIdentityProviderConfig;
 import org.keycloak.social.google.GoogleIdentityProvider;
 import org.keycloak.social.google.GoogleIdentityProviderFactory;
 import org.keycloak.social.linkedin.LinkedInIdentityProvider;
@@ -143,6 +146,8 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
                     assertFacebookIdentityProviderConfig(realm, identityProvider);
                 } else if (GitHubIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
                     assertGitHubIdentityProviderConfig(realm, identityProvider);
+                } else if (PayPalIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
+                    assertPayPalIdentityProviderConfig(realm, identityProvider);
                 } else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
                     assertTwitterIdentityProviderConfig(identityProvider);
                 } else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
@@ -253,6 +258,26 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
         assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
     }
 
+    private void assertPayPalIdentityProviderConfig(RealmModel realm, IdentityProviderModel identityProvider) {
+        PayPalIdentityProvider payPalIdentityProvider = new PayPalIdentityProviderFactory().create(session, identityProvider);
+        PayPalIdentityProviderConfig config = payPalIdentityProvider.getConfig();
+
+        assertEquals("model-paypal", config.getAlias());
+        assertEquals(PayPalIdentityProviderFactory.PROVIDER_ID, config.getProviderId());
+        assertEquals(true, config.isEnabled());
+        assertEquals(false, config.isTrustEmail());
+        assertEquals(false, config.isAuthenticateByDefault());
+        assertEquals(false, config.isStoreToken());
+        assertEquals("clientId", config.getClientId());
+        assertEquals("clientSecret", config.getClientSecret());
+        assertEquals(false, config.targetSandbox());
+        assertEquals(realm.getFlowByAlias(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW).getId(), identityProvider.getFirstBrokerLoginFlowId());
+        assertEquals(realm.getBrowserFlow().getId(), identityProvider.getPostBrokerLoginFlowId());
+        assertEquals(PayPalIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
+        assertEquals(PayPalIdentityProvider.BASE_URL + PayPalIdentityProvider.TOKEN_RESOURCE, config.getTokenUrl());
+        assertEquals(PayPalIdentityProvider.BASE_URL + PayPalIdentityProvider.PROFILE_RESOURCE, config.getUserInfoUrl());
+    }
+
     private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) {
         LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = liIdentityProvider.getConfig();
diff --git a/testsuite/integration-deprecated/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration-deprecated/src/test/resources/broker-test/test-realm-with-broker.json
index 09fa373..8d5f102 100755
--- a/testsuite/integration-deprecated/src/test/resources/broker-test/test-realm-with-broker.json
+++ b/testsuite/integration-deprecated/src/test/resources/broker-test/test-realm-with-broker.json
@@ -52,6 +52,21 @@
             }
         },
         {
+          "alias" : "model-paypal",
+          "providerId" : "paypal",
+          "enabled": true,
+          "storeToken": false,
+          "postBrokerLoginFlowAlias" : "browser",
+          "config": {
+              "sandbox": false,
+              "authorizationUrl": "authorizationUrl",
+              "tokenUrl": "tokenUrl",
+              "userInfoUrl": "userInfoUrl",
+              "clientId": "clientId",
+              "clientSecret": "clientSecret"
+          }
+        },
+        {
             "alias" : "model-twitter",
             "providerId" : "twitter",
             "enabled": true,
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 04b7f7a..3ae3500 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -484,6 +484,8 @@ disableUserInfo=Disable User Info
 identity-provider.disableUserInfo.tooltip=Disable usage of User Info service to obtain additional user information?  Default is to use this OIDC service.
 userIp=Use userIp Param
 identity-provider.google-userIp.tooltip=Set 'userIp' query parameter when invoking on Google's User Info service.  This will use the user's ip address.  Useful if Google is throttling access to the User Info service.
+sandbox=Target Sandbox
+identity-provider.paypal-sandbox.tooltip=Target PayPal's sandbox environment
 update-profile-on-first-login=Update Profile on First Login
 on=On
 on-missing-info=On missing info
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal.html
new file mode 100644
index 0000000..62e97ba
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal.html
@@ -0,0 +1 @@
+<div data-ng-include data-src="resourceUrl + '/partials/realm-identity-provider-social.html'"></div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal-ext.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal-ext.html
new file mode 100644
index 0000000..692a078
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-paypal-ext.html
@@ -0,0 +1,7 @@
+<div class="form-group">
+  <label class="col-md-2 control-label" for="sandbox">{{:: 'sandbox' | translate}}</label>
+  <div class="col-md-6">
+      <input ng-model="identityProvider.config.sandbox" id="sandbox" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+  </div>
+  <kc-tooltip>{{:: 'identity-provider.paypal-sandbox.tooltip' | translate}}</kc-tooltip>
+</div>
\ No newline at end of file