Support profile projection parameter for LinkedIn IDP

Closes #13384
This commit is contained in:
Lex Cao 2022-08-09 00:39:54 +08:00 committed by Bruno Oliveira da Silva
parent 7311e12066
commit 8ea3f30d82
4 changed files with 47 additions and 10 deletions

View file

@ -29,9 +29,6 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Iterator; import java.util.Iterator;
/** /**
@ -50,11 +47,13 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
public static final String EMAIL_SCOPE = "r_emailaddress"; public static final String EMAIL_SCOPE = "r_emailaddress";
public static final String DEFAULT_SCOPE = "r_liteprofile " + EMAIL_SCOPE; public static final String DEFAULT_SCOPE = "r_liteprofile " + EMAIL_SCOPE;
private static final String PROFILE_PROJECTION = "profileProjection";
public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config); super(session, config);
config.setAuthorizationUrl(AUTH_URL); config.setAuthorizationUrl(AUTH_URL);
config.setTokenUrl(TOKEN_URL); config.setTokenUrl(TOKEN_URL);
config.setUserInfoUrl(PROFILE_URL); config.setUserInfoUrl(getUserInfoUrl(config.getConfig().get(PROFILE_PROJECTION)));
// email scope is mandatory in order to resolve the username using the email address // email scope is mandatory in order to resolve the username using the email address
if (!config.getDefaultScope().contains(EMAIL_SCOPE)) { if (!config.getDefaultScope().contains(EMAIL_SCOPE)) {
config.setDefaultScope(config.getDefaultScope() + " " + EMAIL_SCOPE); config.setDefaultScope(config.getDefaultScope() + " " + EMAIL_SCOPE);
@ -68,7 +67,7 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
@Override @Override
protected String getProfileEndpointForValidation(EventBuilder event) { protected String getProfileEndpointForValidation(EventBuilder event) {
return PROFILE_URL; return getConfig().getUserInfoUrl();
} }
@Override @Override
@ -90,7 +89,7 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
log.debug("doGetFederatedIdentity()"); log.debug("doGetFederatedIdentity()");
try { try {
BrokeredIdentityContext identity = extractIdentityFromProfile(null, doHttpGet(PROFILE_URL, accessToken)); BrokeredIdentityContext identity = extractIdentityFromProfile(null, doHttpGet(getConfig().getUserInfoUrl(), accessToken));
identity.setEmail(fetchEmailAddress(accessToken, identity)); identity.setEmail(fetchEmailAddress(accessToken, identity));
@ -152,4 +151,16 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
return null; return null;
} }
/**
* append profileProjection to profile URL if exists
*
* @param projection parameter
* @return Profile URL
*/
private String getUserInfoUrl(String projection) {
return projection == null || projection.isEmpty()
? PROFILE_URL
: PROFILE_URL + "?projection=" + projection;
}
} }

View file

@ -83,6 +83,7 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_HOST
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_NON_MATCHING_HOSTED_DOMAIN; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE_NON_MATCHING_HOSTED_DOMAIN;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.INSTAGRAM; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.INSTAGRAM;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN_WITH_PROJECTION;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT;
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT4; import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.OPENSHIFT4;
@ -122,6 +123,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class), GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class),
TWITTER("twitter", TwitterLoginPage.class), TWITTER("twitter", TwitterLoginPage.class),
LINKEDIN("linkedin", LinkedInLoginPage.class), LINKEDIN("linkedin", LinkedInLoginPage.class),
LINKEDIN_WITH_PROJECTION("linkedin", LinkedInLoginPage.class),
MICROSOFT("microsoft", MicrosoftLoginPage.class), MICROSOFT("microsoft", MicrosoftLoginPage.class),
PAYPAL("paypal", PayPalLoginPage.class), PAYPAL("paypal", PayPalLoginPage.class),
STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class), STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
@ -390,6 +392,16 @@ public class SocialLoginTest extends AbstractKeycloakTest {
assertAccount(); assertAccount();
} }
@Test
public void linkedinLoginWithProjection() {
setTestProvider(LINKEDIN_WITH_PROJECTION);
addAttributeMapper("picture",
"profilePicture.displayImage~.elements[0].identifiers[0].identifier");
performLogin();
assertAccount();
assertAttribute("picture", getConfig("profile.picture"));
}
@Test @Test
public void microsoftLogin() { public void microsoftLogin() {
setTestProvider(MICROSOFT); setTestProvider(MICROSOFT);
@ -432,8 +444,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
if (provider == GOOGLE_NON_MATCHING_HOSTED_DOMAIN) { if (provider == GOOGLE_NON_MATCHING_HOSTED_DOMAIN) {
idp.getConfig().put("hostedDomain", "non-matching-hosted-domain"); idp.getConfig().put("hostedDomain", "non-matching-hosted-domain");
} }
if (provider == LINKEDIN_WITH_PROJECTION) {
idp.getConfig().put("profileProjection", "(id,firstName,lastName,profilePicture(displayImage~:playableStreams))");
}
if (provider == STACKOVERFLOW) { if (provider == STACKOVERFLOW) {
idp.getConfig().put("key", getConfig(provider, "clientKey")); idp.getConfig().put("key", getConfig(provider, "clientKey"));
} }

View file

@ -694,6 +694,8 @@ offlineAccess=Request refresh token
identity-provider.google-offlineAccess.tooltip=Set 'access_type' query parameter to 'offline' when redirecting to google authorization endpoint, to get a refresh token back. Useful if planning to use Token Exchange to retrieve Google token to access Google APIs when the user is not at the browser. identity-provider.google-offlineAccess.tooltip=Set 'access_type' query parameter to 'offline' when redirecting to google authorization endpoint, to get a refresh token back. Useful if planning to use Token Exchange to retrieve Google token to access Google APIs when the user is not at the browser.
hostedDomain=Hosted Domain hostedDomain=Hosted Domain
identity-provider.google-hostedDomain.tooltip=Set 'hd' query parameter when logging in with Google. Google will list accounts only for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered, any hosted account can be used. identity-provider.google-hostedDomain.tooltip=Set 'hd' query parameter when logging in with Google. Google will list accounts only for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered, any hosted account can be used.
profileProjection=Profile Projection
identity-provider.linkedin-profileProjection.tooltip=Projection parameter for profile request. Leave empty for default projection.
identity-provider.facebook-fetchedFields.label=Additional user's profile fields identity-provider.facebook-fetchedFields.label=Additional user's profile fields
identity-provider.facebook-fetchedFields.tooltip=Provide additional fields which would be fetched using the profile request. This will be appended to the default set of 'id,name,email,first_name,last_name'. identity-provider.facebook-fetchedFields.tooltip=Provide additional fields which would be fetched using the profile request. This will be appended to the default set of 'id,name,email,first_name,last_name'.
sandbox=Target Sandbox sandbox=Target Sandbox

View file

@ -0,0 +1,11 @@
<div class="form-group">
<label class="col-md-2 control-label" for="profileProjection">{{:: 'profileProjection' | translate}}</label>
<div class="col-md-6">
<input
ng-model="identityProvider.config.profileProjection"
placeholder="e.g.: (id,firstName,lastName,profilePicture(displayImage~:playableStreams))"
id="profileProjection"
class="form-control" />
</div>
<kc-tooltip>{{:: 'identity-provider.linkedin-profileProjection.tooltip' | translate}}</kc-tooltip>
</div>