feat: added PayPal IDP (#4449)

This commit is contained in:
Petter Lysne 2017-09-12 11:57:59 +02:00 committed by Stian Thorgersen
parent a485d7be53
commit 7f8b5e032a
15 changed files with 321 additions and 2 deletions

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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";
}
}

View file

@ -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

View file

@ -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
org.keycloak.social.bitbucket.BitbucketIdentityProviderFactory

View file

@ -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.

View file

@ -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
}
}
}

View file

@ -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);
@ -190,6 +193,13 @@ public class SocialLoginTest extends AbstractKeycloakTest {
assertAccount();
}
@Test
public void paypalLogin() {
currentTestProvider = PAYPAL;
performLogin();
assertAccount();
}
@Test
public void stackoverflowLogin() {
currentTestProvider = STACKOVERFLOW;
@ -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;
}

View file

@ -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);

View file

@ -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();

View file

@ -51,6 +51,21 @@
"clientSecret": "clientSecret"
}
},
{
"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",

View file

@ -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

View file

@ -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>

View file

@ -0,0 +1 @@
<div data-ng-include data-src="resourceUrl + '/partials/realm-identity-provider-social.html'"></div>