KEYCLOAK-14006 Allow administrator to add additional fields to be fetched with Facebook profile request

This commit is contained in:
Bartosz Siemieńczuk 2020-05-05 10:41:17 +02:00 committed by Bruno Oliveira da Silva
parent de9a0a0a4a
commit e2040f5d13
6 changed files with 99 additions and 14 deletions

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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

View file

@ -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()));

View file

@ -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

View file

@ -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>