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 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2
|
* LinkedIn social provider. See https://developer.linkedin.com/docs/oauth2
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias (velias at redhat dot com)
|
* @author Vlastimil Elias (velias at redhat dot com)
|
||||||
*/
|
*/
|
||||||
public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider<OAuth2IdentityProviderConfig> implements SocialIdentityProvider<OAuth2IdentityProviderConfig> {
|
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 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
@ -168,7 +170,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
||||||
assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
|
assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
|
||||||
config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
|
config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void beforeSocialLoginTest() {
|
public void beforeSocialLoginTest() {
|
||||||
accountPage.setAuthRealm(REALM);
|
accountPage.setAuthRealm(REALM);
|
||||||
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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