KEYCLOAK-14006 Allow administrator to add additional fields to be fetched with Facebook profile request
This commit is contained in:
parent
de9a0a0a4a
commit
e2040f5d13
6 changed files with 99 additions and 14 deletions
|
@ -18,34 +18,29 @@
|
|||
package org.keycloak.social.facebook;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
|
||||
import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
|
||||
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider {
|
||||
public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider<FacebookIdentityProviderConfig> implements SocialIdentityProvider<FacebookIdentityProviderConfig> {
|
||||
|
||||
public static final String AUTH_URL = "https://graph.facebook.com/oauth/authorize";
|
||||
public static final String TOKEN_URL = "https://graph.facebook.com/oauth/access_token";
|
||||
public static final String PROFILE_URL = "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name";
|
||||
public static final String DEFAULT_SCOPE = "email";
|
||||
protected static final String PROFILE_URL_FIELDS_SEPARATOR = ",";
|
||||
|
||||
public FacebookIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
|
||||
public FacebookIdentityProvider(KeycloakSession session, FacebookIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
config.setAuthorizationUrl(AUTH_URL);
|
||||
config.setTokenUrl(TOKEN_URL);
|
||||
|
@ -54,8 +49,11 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
|
|||
|
||||
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
|
||||
try {
|
||||
JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
|
||||
|
||||
final String fetchedFields = getConfig().getFetchedFields();
|
||||
final String url = StringUtil.isNotNull(fetchedFields)
|
||||
? String.join(PROFILE_URL_FIELDS_SEPARATOR, PROFILE_URL, fetchedFields)
|
||||
: PROFILE_URL;
|
||||
JsonNode profile = SimpleHttp.doGet(url, session).header("Authorization", "Bearer " + accessToken).asJson();
|
||||
return extractIdentityFromProfile(null, profile);
|
||||
} catch (Exception e) {
|
||||
throw new IdentityBrokerException("Could not obtain user profile from facebook.", e);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package org.keycloak.social.facebook;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.saml.common.util.StringUtil;
|
||||
|
||||
public class FacebookIdentityProviderConfig extends OIDCIdentityProviderConfig {
|
||||
|
||||
public FacebookIdentityProviderConfig(IdentityProviderModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
public FacebookIdentityProviderConfig() {
|
||||
}
|
||||
|
||||
public String getFetchedFields() {
|
||||
return Optional.ofNullable(getConfig().get("fetchedFields"))
|
||||
.map(fieldsConfig -> fieldsConfig.replaceAll("\\s+",""))
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public void setFetchedFields(final String fetchedFields) {
|
||||
getConfig().put("fetchedFields", fetchedFields);
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ public class FacebookIdentityProviderFactory extends AbstractIdentityProviderFac
|
|||
|
||||
@Override
|
||||
public FacebookIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new FacebookIdentityProvider(session, new OAuth2IdentityProviderConfig(model));
|
||||
return new FacebookIdentityProvider(session, new FacebookIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -8,16 +8,20 @@ import org.junit.Before;
|
|||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderMapperSyncMode;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
@ -65,11 +69,14 @@ import java.util.Properties;
|
|||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.BITBUCKET;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK_INCLUDE_BIRTHDAY;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB_PRIVATE_EMAIL;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB;
|
||||
|
@ -85,6 +92,8 @@ import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.PAYPAL;
|
|||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.STACKOVERFLOW;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.TWITTER;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
|
@ -109,6 +118,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
GOOGLE_HOSTED_DOMAIN("google", "google-hosted-domain", GoogleLoginPage.class),
|
||||
GOOGLE_NON_MATCHING_HOSTED_DOMAIN("google", "google-hosted-domain", GoogleLoginPage.class),
|
||||
FACEBOOK("facebook", FacebookLoginPage.class),
|
||||
FACEBOOK_INCLUDE_BIRTHDAY("facebook", FacebookLoginPage.class),
|
||||
GITHUB("github", GitHubLoginPage.class),
|
||||
GITHUB_PRIVATE_EMAIL("github", "github-private-email", GitHubLoginPage.class),
|
||||
TWITTER("twitter", TwitterLoginPage.class),
|
||||
|
@ -308,6 +318,17 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
testTokenExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
@UncaughtServerErrorExpected
|
||||
public void facebookLoginWithEnhancedScope() throws InterruptedException {
|
||||
setTestProvider(FACEBOOK_INCLUDE_BIRTHDAY);
|
||||
addBirthdayMapper();
|
||||
performLogin();
|
||||
assertAccount();
|
||||
assertBirthdayAttribute();
|
||||
testTokenExchange();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void instagramLogin() throws InterruptedException {
|
||||
setTestProvider(INSTAGRAM);
|
||||
|
@ -401,9 +422,29 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
if (provider == PAYPAL) {
|
||||
idp.getConfig().put("sandbox", getConfig(provider, "sandbox"));
|
||||
}
|
||||
if (provider == FACEBOOK_INCLUDE_BIRTHDAY) {
|
||||
idp.getConfig().put("defaultScope", "public_profile,email,user_birthday");
|
||||
idp.getConfig().put("fetchedFields", "birthday");
|
||||
}
|
||||
return idp;
|
||||
}
|
||||
|
||||
private void addBirthdayMapper() {
|
||||
IdentityProviderResource identityProvider = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id);
|
||||
IdentityProviderRepresentation identityProviderRepresentation = identityProvider.toRepresentation();
|
||||
//Add birthday mapper
|
||||
IdentityProviderMapperRepresentation mapperRepresentation = new IdentityProviderMapperRepresentation();
|
||||
mapperRepresentation.setName(currentTestProvider.id + "-birthday-mapper");
|
||||
mapperRepresentation.setIdentityProviderAlias(identityProviderRepresentation.getAlias());
|
||||
mapperRepresentation.setIdentityProviderMapper(currentTestProvider.id + "-user-attribute-mapper");
|
||||
mapperRepresentation.setConfig(ImmutableMap.<String, String>builder()
|
||||
.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.IMPORT.toString())
|
||||
.put(AbstractJsonUserAttributeMapper.CONF_JSON_FIELD, "birthday")
|
||||
.put(AbstractJsonUserAttributeMapper.CONF_USER_ATTRIBUTE, currentTestProvider.id + "_birthday")
|
||||
.build());
|
||||
identityProvider.addMapper(mapperRepresentation).close();
|
||||
}
|
||||
|
||||
private String getConfig(Provider provider, String key) {
|
||||
String providerKey = provider.configId() + "." + key;
|
||||
return System.getProperty("social." + providerKey, config.getProperty(providerKey, config.getProperty("common." + key)));
|
||||
|
@ -449,6 +490,15 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
assertEquals(getConfig("profile.email"), accountPage.getEmail());
|
||||
}
|
||||
|
||||
private void assertBirthdayAttribute() {
|
||||
List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
|
||||
assertEquals(1, users.size());
|
||||
assertNotNull(users.get(0).getAttributes());
|
||||
final String birthdayAttributeKey = currentTestProvider.id + "_birthday";
|
||||
assertNotNull(users.get(0).getAttributes().get(birthdayAttributeKey));
|
||||
assertEquals(getConfig("profile.birthday"), users.get(0).getAttributes().get(birthdayAttributeKey).get(0));
|
||||
}
|
||||
|
||||
private void assertUpdateProfile(boolean firstName, boolean lastName, boolean email) {
|
||||
assertTrue(URLUtils.currentUrlDoesntStartWith(accountPage.toString()));
|
||||
|
||||
|
|
|
@ -582,6 +582,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.
|
||||
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
|
||||
identity-provider.paypal-sandbox.tooltip=Target PayPal's sandbox environment
|
||||
update-profile-on-first-login=Update Profile on First Login
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="fetchedFields">{{:: 'identity-provider.facebook-fetchedFields.label' | translate}}</label>
|
||||
<div class="col-md-6">
|
||||
<input ng-model="identityProvider.config.fetchedFields" id="fetchedFields" class="form-control" />
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'identity-provider.facebook-fetchedFields.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
Loading…
Reference in a new issue