From c21d110b4c30f2887157e754587aa09494d15b92 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Fri, 20 Mar 2015 15:28:40 +0100 Subject: [PATCH] KEYCLOAK-28 - Login with LinkedIn --- README.md | 2 +- dependencies/server-all/pom.xml | 5 + distribution/modules/build.xml | 4 + .../keycloak/keycloak-server/main/module.xml | 1 + .../keycloak-services/main/module.xml | 1 + .../keycloak-social-linkedin/main/module.xml | 22 ++++ .../WEB-INF/jboss-deployment-structure.xml | 1 + examples/cors/angular-product-app/.gitignore | 1 + .../realm-identity-provider-linkedin.html | 1 + social/linkedin/.gitignore | 1 + social/linkedin/pom.xml | 40 +++++++ .../linkedin/LinkedInIdentityProvider.java | 109 ++++++++++++++++++ .../LinkedInIdentityProviderFactory.java | 47 ++++++++ ...cloak.social.SocialIdentityProviderFactory | 1 + social/pom.xml | 1 + .../AbstractIdentityProviderModelTest.java | 2 + .../broker/ImportIdentityProviderTest.java | 21 ++++ .../broker-test/test-realm-with-broker.json | 16 ++- 18 files changed, 274 insertions(+), 2 deletions(-) create mode 100755 distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-linkedin/main/module.xml create mode 100644 examples/cors/angular-product-app/.gitignore create mode 100755 forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-linkedin.html create mode 100644 social/linkedin/.gitignore create mode 100755 social/linkedin/pom.xml create mode 100755 social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java create mode 100644 social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java create mode 100644 social/linkedin/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory diff --git a/README.md b/README.md index 3ba55db30d..1d05652d9e 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ It can be used for social applications as well as enterprise applications. It i Here's some of the features: * SSO and Single Log Out for browser applications -* Social Broker. Enable Google, Facebook, Yahoo, Twitter social login with no code required. +* Social Broker. Enable Google, Facebook, Yahoo, Twitter, GitHub, LinkedIn social login with no code required. * Optional LDAP/Active Directory integration * Optional User Registration * Password and TOTP support (via Google Authenticator or FreeOTP). Client cert auth coming soon. diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index fe6d5cc4d9..a7bd5223bb 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -122,6 +122,11 @@ keycloak-social-facebook ${project.version} + + org.keycloak + keycloak-social-linkedin + ${project.version} + diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 919916509f..ad0f9e7c11 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -235,6 +235,10 @@ + + + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml index c757599aaf..f8a251d7a5 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml @@ -57,6 +57,7 @@ + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index bafc53f450..6166b9b6ac 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -60,6 +60,7 @@ + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-linkedin/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-linkedin/main/module.xml new file mode 100755 index 0000000000..00853bf3a3 --- /dev/null +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-linkedin/main/module.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 29de32e663..6a97ca5866 100755 --- a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -50,6 +50,7 @@ + diff --git a/examples/cors/angular-product-app/.gitignore b/examples/cors/angular-product-app/.gitignore new file mode 100644 index 0000000000..950095761c --- /dev/null +++ b/examples/cors/angular-product-app/.gitignore @@ -0,0 +1 @@ +/.externalToolBuilders/* diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-linkedin.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-linkedin.html new file mode 100755 index 0000000000..a4630ac786 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-linkedin.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/social/linkedin/.gitignore b/social/linkedin/.gitignore new file mode 100644 index 0000000000..b83d22266a --- /dev/null +++ b/social/linkedin/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/social/linkedin/pom.xml b/social/linkedin/pom.xml new file mode 100755 index 0000000000..d9fd60387e --- /dev/null +++ b/social/linkedin/pom.xml @@ -0,0 +1,40 @@ + + + keycloak-social-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../pom.xml + + 4.0.0 + jar + + keycloak-social-linkedin + Keycloak Social LinkedIn + + + + + org.keycloak + keycloak-social-core + ${project.version} + provided + + + org.keycloak + keycloak-broker-oidc + ${project.version} + provided + + + org.codehaus.jackson + jackson-mapper-asl + provided + + + org.jboss.logging + jboss-logging + provided + + + diff --git a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java new file mode 100755 index 0000000000..298612e6b1 --- /dev/null +++ b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java @@ -0,0 +1,109 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * 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.linkedin; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; + +import org.codehaus.jackson.JsonNode; +import org.jboss.logging.Logger; +import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.util.SimpleHttp; +import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.social.SocialIdentityProvider; + +/** + * LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2 + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { + + private static final Logger log = Logger.getLogger(LinkedInIdentityProvider.class); + + public static final String AUTH_URL = "https://www.linkedin.com/uas/oauth2/authorization"; + public static final String TOKEN_URL = "https://www.linkedin.com/uas/oauth2/accessToken"; + public static final String PROFILE_URL = "https://api.linkedin.com/v1/people/~:(id,formatted-name,email-address,public-profile-url)?format=json"; + public static final String DEFAULT_SCOPE = "r_basicprofile r_emailaddress"; + + public LinkedInIdentityProvider(OAuth2IdentityProviderConfig config) { + super(config); + config.setAuthorizationUrl(AUTH_URL); + config.setTokenUrl(TOKEN_URL); + config.setUserInfoUrl(PROFILE_URL); + } + + @Override + protected FederatedIdentity doGetFederatedIdentity(String accessToken) { + log.debug("doGetFederatedIdentity()"); + try { + JsonNode profile = SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken).asJson(); + + FederatedIdentity user = new FederatedIdentity(getJsonProperty(profile, "id")); + + user.setUsername(extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"))); + user.setName(getJsonProperty(profile, "formattedName")); + user.setEmail(getJsonProperty(profile, "emailAddress")); + + return user; + } catch (Exception e) { + throw new IdentityBrokerException("Could not obtain user profile from github.", e); + } + } + + protected static String extractUsernameFromProfileURL(String profileURL) { + if (isNotBlank(profileURL)) { + + try { + log.debug("go to extract username from profile URL " + profileURL); + URL u = new URL(profileURL); + String path = u.getPath(); + if (isNotBlank(path) && path.length() > 1) { + if (path.startsWith("/")) { + path = path.substring(1); + } + String[] pe = path.split("/"); + if (pe.length >= 2) { + return URLDecoder.decode(pe[1], "UTF-8"); + } else { + log.warn("LinkedIn profile URL path is without second part: " + profileURL); + } + } else { + log.warn("LinkedIn profile URL is without path part: " + profileURL); + } + } catch (MalformedURLException e) { + log.warn("LinkedIn profile URL is malformed: " + profileURL); + } catch (Exception e) { + log.warn("LinkedIn profile URL " + profileURL + " username extraction failed due: " + e.getMessage()); + } + } + return null; + } + + private static boolean isNotBlank(String s) { + return s != null && s.trim().length() > 0; + } + + @Override + protected String getDefaultScopes() { + return DEFAULT_SCOPE; + } +} diff --git a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java new file mode 100644 index 0000000000..958a513922 --- /dev/null +++ b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProviderFactory.java @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * 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.linkedin; + +import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.social.SocialIdentityProviderFactory; + +/** + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFactory + implements SocialIdentityProviderFactory { + + public static final String PROVIDER_ID = "linkedin"; + + @Override + public String getName() { + return "LinkedIn"; + } + + @Override + public LinkedInIdentityProvider create(IdentityProviderModel model) { + return new LinkedInIdentityProvider(new OAuth2IdentityProviderConfig(model)); + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/social/linkedin/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory b/social/linkedin/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory new file mode 100644 index 0000000000..5ffef9749b --- /dev/null +++ b/social/linkedin/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory @@ -0,0 +1 @@ +org.keycloak.social.linkedin.LinkedInIdentityProviderFactory \ No newline at end of file diff --git a/social/pom.xml b/social/pom.xml index 386a215d77..ded7c60eed 100755 --- a/social/pom.xml +++ b/social/pom.xml @@ -20,6 +20,7 @@ google twitter facebook + linkedin diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java index 2dd3164575..84bbc9c51f 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java @@ -24,6 +24,7 @@ import org.keycloak.social.facebook.FacebookIdentityProviderFactory; import org.keycloak.social.github.GitHubIdentityProviderFactory; import org.keycloak.social.google.GoogleIdentityProviderFactory; import org.keycloak.social.twitter.TwitterIdentityProviderFactory; +import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory; import org.keycloak.testsuite.model.AbstractModelTest; import java.util.Collections; @@ -47,6 +48,7 @@ public abstract class AbstractIdentityProviderModelTest extends AbstractModelTes this.expectedProviders.add(FacebookIdentityProviderFactory.PROVIDER_ID); this.expectedProviders.add(GitHubIdentityProviderFactory.PROVIDER_ID); this.expectedProviders.add(TwitterIdentityProviderFactory.PROVIDER_ID); + this.expectedProviders.add(LinkedInIdentityProviderFactory.PROVIDER_ID); this.expectedProviders = Collections.unmodifiableSet(this.expectedProviders); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java index d2caacfa77..f596f1b272 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java @@ -38,6 +38,8 @@ import org.keycloak.social.google.GoogleIdentityProvider; import org.keycloak.social.google.GoogleIdentityProviderFactory; import org.keycloak.social.twitter.TwitterIdentityProvider; import org.keycloak.social.twitter.TwitterIdentityProviderFactory; +import org.keycloak.social.linkedin.LinkedInIdentityProvider; +import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory; import java.io.IOException; import java.util.HashSet; @@ -160,6 +162,8 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertGitHubIdentityProviderConfig(identityProvider); } else if (TwitterIdentityProviderFactory.PROVIDER_ID.equals(providerId)) { assertTwitterIdentityProviderConfig(identityProvider); + } else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) { + assertLinkedInIdentityProviderConfig(identityProvider); } else { continue; } @@ -257,6 +261,23 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes assertEquals(GitHubIdentityProvider.PROFILE_URL, config.getUserInfoUrl()); } + private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) { + LinkedInIdentityProvider gitHubIdentityProvider = new LinkedInIdentityProviderFactory().create(identityProvider); + OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig(); + + assertEquals("model-linkedin", config.getAlias()); + assertEquals(LinkedInIdentityProviderFactory.PROVIDER_ID, config.getProviderId()); + assertEquals(true, config.isEnabled()); + assertEquals(true, config.isUpdateProfileFirstLogin()); + assertEquals(false, config.isAuthenticateByDefault()); + assertEquals(false, config.isStoreToken()); + assertEquals("clientId", config.getClientId()); + assertEquals("clientSecret", config.getClientSecret()); + assertEquals(LinkedInIdentityProvider.AUTH_URL, config.getAuthorizationUrl()); + assertEquals(LinkedInIdentityProvider.TOKEN_URL, config.getTokenUrl()); + assertEquals(LinkedInIdentityProvider.PROFILE_URL, config.getUserInfoUrl()); + } + private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) { TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(identityProvider); OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig(); diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json index d08df51d95..96cdc9602f 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json +++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json @@ -61,6 +61,20 @@ "clientSecret": "clientSecret" } }, + { + "alias" : "model-linkedin", + "providerId" : "linkedin", + "enabled": true, + "updateProfileFirstLogin" : "true", + "storeToken": false, + "config": { + "authorizationUrl": "authorizationUrl", + "tokenUrl": "tokenUrl", + "userInfoUrl": "userInfoUrl", + "clientId": "clientId", + "clientSecret": "clientSecret" + } + }, { "alias" : "model-saml-signed-idp", "providerId" : "saml", @@ -213,4 +227,4 @@ } ] } -} \ No newline at end of file +}