diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
index 9c2eef6..65406a9 100755
--- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
@@ -28,9 +28,11 @@ import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
+import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
+import java.util.Iterator;
/**
* LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2
@@ -43,14 +45,20 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
public static final String AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization";
public static final String TOKEN_URL = "https://www.linkedin.com/oauth/v2/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 static final String PROFILE_URL = "https://api.linkedin.com/v2/me";
+ public static final String EMAIL_URL = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))";
+ public static final String EMAIL_SCOPE = "r_emailaddress";
+ public static final String DEFAULT_SCOPE = "r_liteprofile " + EMAIL_SCOPE;
public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL);
+ // email scope is mandatory in order to resolve the username using the email address
+ if (!config.getDefaultScope().contains(EMAIL_SCOPE)) {
+ config.setDefaultScope(config.getDefaultScope() + " " + EMAIL_SCOPE);
+ }
}
@Override
@@ -67,17 +75,14 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
- String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
- user.setUsername(username);
- user.setName(getJsonProperty(profile, "formattedName"));
- user.setEmail(getJsonProperty(profile, "emailAddress"));
+ user.setFirstName(getFirstMultiLocaleString(profile, "firstName"));
+ user.setLastName(getFirstMultiLocaleString(profile, "lastName"));
user.setIdpConfig(getConfig());
user.setIdp(this);
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
return user;
-
}
@@ -85,48 +90,66 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()");
try {
- JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
- return extractIdentityFromProfile(null, profile);
+ BrokeredIdentityContext identity = extractIdentityFromProfile(null, doHttpGet(PROFILE_URL, accessToken));
+
+ identity.setEmail(fetchEmailAddress(accessToken, identity));
+
+ if (identity.getUsername() == null) {
+ identity.setUsername(identity.getEmail());
+ }
+
+ return identity;
} catch (Exception e) {
throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e);
}
}
- protected static String extractUsernameFromProfileURL(String profileURL) {
- if (isNotBlank(profileURL)) {
+ @Override
+ protected String getDefaultScopes() {
+ return DEFAULT_SCOPE;
+ }
+ private String fetchEmailAddress(String accessToken, BrokeredIdentityContext identity) {
+ if (identity.getEmail() == null && getConfig().getDefaultScope() != null && getConfig().getDefaultScope().contains(EMAIL_SCOPE)) {
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);
+ JsonNode emailAddressNode = doHttpGet(EMAIL_URL, accessToken).findPath("emailAddress");
+
+ if (emailAddressNode != null) {
+ return emailAddressNode.asText();
}
- } 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());
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to retrieve user email", cause);
}
}
+
return null;
}
- private static boolean isNotBlank(String s) {
- return s != null && s.trim().length() > 0;
+ private JsonNode doHttpGet(String url, String bearerToken) throws IOException {
+ JsonNode response = SimpleHttp.doGet(url, session).header("Authorization", "Bearer " + bearerToken).asJson();
+
+ if (response.hasNonNull("serviceErrorCode")) {
+ throw new IdentityBrokerException("Could not obtain response from [" + url + "]. Response from server: " + response);
+ }
+
+ return response;
}
- @Override
- protected String getDefaultScopes() {
- return DEFAULT_SCOPE;
+ private String getFirstMultiLocaleString(JsonNode node, String name) {
+ JsonNode claim = node.get(name);
+
+ if (claim != null) {
+ JsonNode localized = claim.get("localized");
+
+ if (localized != null) {
+ Iterator<JsonNode> iterator = localized.iterator();
+
+ if (iterator.hasNext()) {
+ return iterator.next().asText();
+ }
+ }
+ }
+
+ return null;
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/LinkedInLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/LinkedInLoginPage.java
index 70cb1eb..cf1b796 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/LinkedInLoginPage.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/LinkedInLoginPage.java
@@ -25,13 +25,13 @@ import org.openqa.selenium.support.FindBy;
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class LinkedInLoginPage extends AbstractSocialLoginPage {
- @FindBy(id = "session_key-login")
+ @FindBy(id = "username")
private WebElement usernameInput;
- @FindBy(id = "session_password-login")
+ @FindBy(id = "password")
private WebElement passwordInput;
- @FindBy(name = "signin")
+ @FindBy(xpath = "//button[text() = 'Sign in']")
private WebElement loginButton;
@FindBy(name = "action")