parent
7311e12066
commit
8ea3f30d82
4 changed files with 47 additions and 10 deletions
|
@ -29,14 +29,11 @@ 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
|
||||
*
|
||||
*
|
||||
* @author Vlastimil Elias (velias at redhat dot com)
|
||||
*/
|
||||
public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {
|
||||
|
@ -50,11 +47,13 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
|
|||
public static final String EMAIL_SCOPE = "r_emailaddress";
|
||||
public static final String DEFAULT_SCOPE = "r_liteprofile " + EMAIL_SCOPE;
|
||||
|
||||
private static final String PROFILE_PROJECTION = "profileProjection";
|
||||
|
||||
public LinkedInIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
config.setAuthorizationUrl(AUTH_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
|
||||
if (!config.getDefaultScope().contains(EMAIL_SCOPE)) {
|
||||
config.setDefaultScope(config.getDefaultScope() + " " + EMAIL_SCOPE);
|
||||
|
@ -68,7 +67,7 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
|
|||
|
||||
@Override
|
||||
protected String getProfileEndpointForValidation(EventBuilder event) {
|
||||
return PROFILE_URL;
|
||||
return getConfig().getUserInfoUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -90,7 +89,7 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
|
|||
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
|
||||
log.debug("doGetFederatedIdentity()");
|
||||
try {
|
||||
BrokeredIdentityContext identity = extractIdentityFromProfile(null, doHttpGet(PROFILE_URL, accessToken));
|
||||
BrokeredIdentityContext identity = extractIdentityFromProfile(null, doHttpGet(getConfig().getUserInfoUrl(), accessToken));
|
||||
|
||||
identity.setEmail(fetchEmailAddress(accessToken, identity));
|
||||
|
||||
|
@ -152,4 +151,16 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAu
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.INSTAGRAM;
|
||||
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.OPENSHIFT;
|
||||
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),
|
||||
TWITTER("twitter", TwitterLoginPage.class),
|
||||
LINKEDIN("linkedin", LinkedInLoginPage.class),
|
||||
LINKEDIN_WITH_PROJECTION("linkedin", LinkedInLoginPage.class),
|
||||
MICROSOFT("microsoft", MicrosoftLoginPage.class),
|
||||
PAYPAL("paypal", PayPalLoginPage.class),
|
||||
STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
|
||||
|
@ -168,7 +170,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
|
||||
config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
|
||||
}
|
||||
|
||||
|
||||
@Before
|
||||
public void beforeSocialLoginTest() {
|
||||
accountPage.setAuthRealm(REALM);
|
||||
|
@ -390,6 +392,16 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
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
|
||||
public void microsoftLogin() {
|
||||
setTestProvider(MICROSOFT);
|
||||
|
@ -432,8 +444,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
if (provider == GOOGLE_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) {
|
||||
idp.getConfig().put("key", getConfig(provider, "clientKey"));
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
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.
|
||||
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.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
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue