From c6e0195a3c67246c0a71209ec48f2cc629623d3e Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Tue, 9 Jun 2015 13:49:58 +0200 Subject: [PATCH] KEYCLOAK-1373 - added attribute importer for other social providers, documented --- .../broker/oidc/OIDCIdentityProvider.java | 4 +- .../AbstractJsonUserAttributeMapper.java | 255 +++++++++++------- .../AbstractJsonUserAttributeMapperTest.java | 120 +++++++++ .../en/en-US/modules/identity-broker.xml | 18 ++ .../facebook/FacebookIdentityProvider.java | 95 +++---- .../facebook/FacebookUserAttributeMapper.java | 29 ++ ...oak.broker.provider.IdentityProviderMapper | 1 + .../social/github/GitHubIdentityProvider.java | 2 +- .../google/GoogleUserAttributeMapper.java | 29 ++ ...oak.broker.provider.IdentityProviderMapper | 1 + .../linkedin/LinkedInIdentityProvider.java | 17 +- .../linkedin/LinkedInUserAttributeMapper.java | 29 ++ ...oak.broker.provider.IdentityProviderMapper | 1 + .../StackoverflowIdentityProvider.java | 21 +- .../StackoverflowUserAttributeMapper.java | 29 ++ ...oak.broker.provider.IdentityProviderMapper | 1 + 16 files changed, 495 insertions(+), 157 deletions(-) create mode 100644 broker/oidc/src/test/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapperTest.java create mode 100644 social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUserAttributeMapper.java create mode 100755 social/facebook/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper create mode 100644 social/google/src/main/java/org/keycloak/social/google/GoogleUserAttributeMapper.java create mode 100755 social/google/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper create mode 100644 social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInUserAttributeMapper.java create mode 100755 social/linkedin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper create mode 100644 social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowUserAttributeMapper.java create mode 100755 social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index 01e6c418bd..c576a5d6cd 100755 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -19,6 +19,7 @@ package org.keycloak.broker.oidc; import org.codehaus.jackson.JsonNode; import org.jboss.logging.Logger; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.util.JsonSimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.AuthenticationRequest; @@ -50,6 +51,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; + import java.io.IOException; import java.security.PublicKey; @@ -224,7 +226,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider configProperties = new ArrayList(); + /** + * Config param where name of mapping source JSON User Profile field is stored. + */ + public static final String CONF_JSON_FIELD = "jsonField"; + /** + * Config param where name of mapping target USer attribute is stored. + */ + public static final String CONF_USER_ATTRIBUTE = "userAttribute"; - static { - ProviderConfigProperty property; - ProviderConfigProperty property1; - property1 = new ProviderConfigProperty(); - property1.setName(CONF_JSON_FIELD); - property1.setLabel("Social Profile JSON Field Name"); - property1.setHelpText("Name of field in Social provider User Profile JSON data to get value from."); - property1.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property1); - property = new ProviderConfigProperty(); - property.setName(CONF_USER_ATTRIBUTE); - property.setLabel("User Attribute Name"); - property.setHelpText("User attribute name to store information into."); - property.setType(ProviderConfigProperty.STRING_TYPE); - configProperties.add(property); - } + /** + * Key in {@link BrokeredIdentityContext#getContextData()} where {@link JsonNode} with user profile is stored. + */ + public static final String CONTEXT_JSON_NODE = OIDCIdentityProvider.USER_INFO; - public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile) { - user.getContextData().put(AbstractJsonUserAttributeMapper.CONTEXT_JSON_NODE, profile); - if (LOGGER_DUMP_USER_PROFILE.isDebugEnabled()) - LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data: " + profile); - } + private static final List configProperties = new ArrayList(); - @Override - public List getConfigProperties() { - return configProperties; - } + static { + ProviderConfigProperty property; + ProviderConfigProperty property1; + property1 = new ProviderConfigProperty(); + property1.setName(CONF_JSON_FIELD); + property1.setLabel("Social Profile JSON Field Path"); + property1.setHelpText("Path of field in Social provider User Profile JSON data to get value from. You can use dot notation for nesting and square brackets for array index. Eg. 'contact.address[0].country'."); + property1.setType(ProviderConfigProperty.STRING_TYPE); + configProperties.add(property1); + property = new ProviderConfigProperty(); + property.setName(CONF_USER_ATTRIBUTE); + property.setLabel("User Attribute Name"); + property.setHelpText("User attribute name to store information into."); + property.setType(ProviderConfigProperty.STRING_TYPE); + configProperties.add(property); + } - @Override - public String getDisplayCategory() { - return "Attribute Importer"; - } + /** + * Store used profile JsonNode into user context for later use by this mapper. Profile data are dumped into special logger if enabled also to allow investigation of the structure. + * + * @param user context to store profile data into + * @param profile to store into context + * @param provider identification of social provider to be used in log dump + * + * @see #importNewUser(KeycloakSession, RealmModel, UserModel, IdentityProviderMapperModel, BrokeredIdentityContext) + * @see BrokeredIdentityContext#getContextData() + */ + public static void storeUserProfileForMapper(BrokeredIdentityContext user, JsonNode profile, String provider) { + user.getContextData().put(AbstractJsonUserAttributeMapper.CONTEXT_JSON_NODE, profile); + if (LOGGER_DUMP_USER_PROFILE.isDebugEnabled()) + LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data for provider "+provider+": " + profile); + } - @Override - public String getDisplayType() { - return "Attribute Importer"; - } + @Override + public List getConfigProperties() { + return configProperties; + } - @Override - public String getHelpText() { - return "Import user profile information if it exists in Social provider JSON data into the specified user attribute."; - } + @Override + public String getDisplayCategory() { + return "Attribute Importer"; + } - @Override - public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE); - if (attribute == null) { - logger.debug("Attribute is not configured"); - return; - } + @Override + public String getDisplayType() { + return "Attribute Importer"; + } - String value = getJsonValue(mapperModel, context); - if (value != null) { - user.setAttribute(attribute, value); - } - } + @Override + public String getHelpText() { + return "Import user profile information if it exists in Social provider JSON data into the specified user attribute."; + } - @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - // we do not update user profile from social provider - } + @Override + public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE); + if (attribute == null || attribute.trim().isEmpty()) { + logger.debug("Attribute is not configured"); + return; + } + attribute = attribute.trim(); - protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String value = getJsonValue(mapperModel, context); + if (value != null) { + user.setAttribute(attribute, value); + } + } - String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD); - if (jsonField == null) { - logger.debug("JSON field is not configured"); - return null; - } + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + // we do not update user profile from social provider + } - JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE); + protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - if (profileJsonNode != null) { - JsonNode value = profileJsonNode.get(jsonField); - if (value != null) { - String ret = value.asText(); - if (ret != null && !ret.trim().isEmpty()) - return ret.trim(); - else - return null; - } - } else { - logger.debug("User profile JSON node is not available."); - } + String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD); + if (jsonField == null || jsonField.trim().isEmpty()) { + logger.debug("JSON field path is not configured"); + return null; + } + jsonField = jsonField.trim(); - return null; - } + if (jsonField.startsWith(JSON_PATH_DELIMITER) || jsonField.endsWith(JSON_PATH_DELIMITER) || jsonField.startsWith("[")) { + logger.debug("JSON field path is invalid " + jsonField); + return null; + } + + JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE); + + String value = getJsonValue(profileJsonNode, jsonField); + + if (value == null) { + logger.debug("User profile JSON value '" + jsonField + "' is not available."); + } + + return value; + } + + protected static String getJsonValue(JsonNode baseNode, String fieldPath) { + logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode); + if (baseNode != null) { + + int idx = fieldPath.indexOf(JSON_PATH_DELIMITER); + + String currentFieldName = fieldPath; + if (idx > 0) { + currentFieldName = fieldPath.substring(0, idx).trim(); + if (currentFieldName.isEmpty()) { + logger.debug("JSON path is invalid " + fieldPath); + return null; + } + } + + String currentNodeName = currentFieldName; + int arrayIndex = -1; + if (currentFieldName.endsWith("]")) { + int bi = currentFieldName.indexOf("["); + if (bi == -1) { + logger.debug("Invalid array index construct in " + currentFieldName); + return null; + } + try { + String is = currentFieldName.substring(bi+1, currentFieldName.length() - 1).trim(); + arrayIndex = Integer.parseInt(is); + } catch (Exception e) { + logger.debug("Invalid array index construct in " + currentFieldName); + return null; + } + currentNodeName = currentFieldName.substring(0,bi).trim(); + } + + JsonNode currentNode = baseNode.get(currentNodeName); + if (arrayIndex > -1 && currentNode.isArray()) { + logger.debug("Going to take array node at index " + arrayIndex); + currentNode = currentNode.get(arrayIndex); + } + + if (currentNode == null) { + logger.debug("JsonNode not found for name " + currentFieldName); + return null; + } + + if (idx < 0) { + if (!currentNode.isValueNode()) { + logger.debug("JsonNode is not value node for name " + currentFieldName); + return null; + } + String ret = currentNode.asText(); + if (ret != null && !ret.trim().isEmpty()) + return ret.trim(); + } else { + return getJsonValue(currentNode, fieldPath.substring(idx + 1)); + } + } + return null; + } } diff --git a/broker/oidc/src/test/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapperTest.java b/broker/oidc/src/test/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapperTest.java new file mode 100644 index 0000000000..dcd1abebd8 --- /dev/null +++ b/broker/oidc/src/test/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapperTest.java @@ -0,0 +1,120 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.broker.oidc.mappers; + +import java.io.IOException; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit test for {@link AbstractJsonUserAttributeMapper} + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class AbstractJsonUserAttributeMapperTest { + + private static ObjectMapper mapper = new ObjectMapper(); + + private static JsonNode baseNode; + + private JsonNode getJsonNode() throws JsonProcessingException, IOException { + if (baseNode == null) + baseNode = mapper.readTree("{ \"value1\" : \"v1 \",\"value_empty\" : \"\", \"value_b\" : true, \"value_i\" : 454, " + " \"value_array\":[\"a1\",\"a2\"], " +" \"nest1\": {\"value1\": \" fgh \",\"value_empty\" : \"\", \"nest2\":{\"value_b\" : false, \"value_i\" : 43}}, "+ " \"nesta\": { \"a\":[{\"av1\": \"vala1\"},{\"av1\": \"vala2\"}]}"+" }"); + return baseNode; + } + + @Test + public void getJsonValue_invalidPath() throws JsonProcessingException, IOException { + + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), ".")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "..")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "...value1")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), ".value1")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1.")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "[]")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "[value1")); + } + + @Test + public void getJsonValue_simpleValues() throws JsonProcessingException, IOException { + + //unknown field returns null + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_unknown")); + + // we check value is trimmed also! + Assert.assertEquals("v1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_empty")); + + Assert.assertEquals("true", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_b")); + Assert.assertEquals("454", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_i")); + + } + + @Test + public void getJsonValue_nestedSimpleValues() throws JsonProcessingException, IOException { + + // null if path points to JSON object + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2")); + + //unknown field returns null + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_unknown")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_unknown")); + + // we check value is trimmed also! + Assert.assertEquals("fgh", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.value_empty")); + + Assert.assertEquals("false", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_b")); + Assert.assertEquals("43", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.value_i")); + + // null if invalid nested path + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nest1.nest2.")); + } + + @Test + public void getJsonValue_simpleArray() throws JsonProcessingException, IOException { + + // array field itself returns null if no index is provided + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array")); + // outside index returns null + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[2]")); + + //corect index + Assert.assertEquals("a1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[0]")); + Assert.assertEquals("a2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[1]")); + + //incorrect array constructs + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[]")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array]")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[a]")); + Assert.assertNull(AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "value_array[-2]")); + } + + @Test + public void getJsonValue_nestedArrayWithObjects() throws JsonProcessingException, IOException { + Assert.assertEquals("vala1", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0].av1")); + Assert.assertEquals("vala2", AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[1].av1")); + + //different path erros or nonexisting indexes or fields return null + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[2].av1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0]")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[0].av_unknown")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[].av1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a.av1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a].av1")); + Assert.assertEquals(null, AbstractJsonUserAttributeMapper.getJsonValue(getJsonNode(), "nesta.a[.av1")); + + } + +} diff --git a/docbook/reference/en/en-US/modules/identity-broker.xml b/docbook/reference/en/en-US/modules/identity-broker.xml index 75df2c7572..2288ed75de 100755 --- a/docbook/reference/en/en-US/modules/identity-broker.xml +++ b/docbook/reference/en/en-US/modules/identity-broker.xml @@ -1246,6 +1246,24 @@ keycloak.createLoginUrl({ the tool tips to see what each mapper can do for you. + +
+ Mapping/Importing User profile data from Social Identity Provider + + You can import user profile data provided by social identity providers like Google, GitHub, LinkedIn, Stackoverflow and Facebook + into new Keycloak user created from given social accounts. After you configure a broker, you'll see a Mappers + button appear. Click on that and you'll get to the list of mappers that are assigned to this broker. There is a + Create button on this page. Clicking on this create button allows you to create a broker mapper. + "Attribute Importer" mapper allows you to define path in JSON user profile data provided by the provider to get value from. + You can use dot notation for nesting and square brackets to access fields in array by index. For example 'contact.address[0].country'. + Then you can define name of Keycloak's user profile attribute this value is stored into. + + + To investigate structure of user profile JSON data provided by social providers you can enable DEBUG level for + logger org.keycloak.social.user_profile_dump and login using given provider. Then you can find user profile + JSON structure in Keycloak log file. + +
Examples diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index ffce087c24..2c062124f3 100755 --- a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -3,10 +3,11 @@ package org.keycloak.social.facebook; import org.codehaus.jackson.JsonNode; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.util.JsonSimpleHttp; -import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.social.SocialIdentityProvider; /** @@ -14,63 +15,65 @@ import org.keycloak.social.SocialIdentityProvider; */ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { - 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"; - public static final String DEFAULT_SCOPE = "email"; + 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"; + public static final String DEFAULT_SCOPE = "email"; - public FacebookIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); - config.setAuthorizationUrl(AUTH_URL); - config.setTokenUrl(TOKEN_URL); - config.setUserInfoUrl(PROFILE_URL); - } + public FacebookIdentityProvider(OAuth2IdentityProviderConfig config) { + super(config); + config.setAuthorizationUrl(AUTH_URL); + config.setTokenUrl(TOKEN_URL); + config.setUserInfoUrl(PROFILE_URL); + } - protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { - try { - JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken)); + protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { + try { + JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken)); - String id = getJsonProperty(profile, "id"); + String id = getJsonProperty(profile, "id"); - BrokeredIdentityContext user = new BrokeredIdentityContext(id); + BrokeredIdentityContext user = new BrokeredIdentityContext(id); - String email = getJsonProperty(profile, "email"); + String email = getJsonProperty(profile, "email"); - user.setEmail(email); + user.setEmail(email); - String username = getJsonProperty(profile, "username"); + String username = getJsonProperty(profile, "username"); - if (username == null) { - if (email != null) { - username = email; - } else { - username = id; - } - } + if (username == null) { + if (email != null) { + username = email; + } else { + username = id; + } + } - user.setUsername(username); + user.setUsername(username); - String firstName = getJsonProperty(profile, "first_name"); - String lastName = getJsonProperty(profile, "last_name"); + String firstName = getJsonProperty(profile, "first_name"); + String lastName = getJsonProperty(profile, "last_name"); - if (lastName == null) { - lastName = ""; - } else { - lastName = " " + lastName; - } + if (lastName == null) { + lastName = ""; + } else { + lastName = " " + lastName; + } - user.setName(firstName + lastName); - user.setIdpConfig(getConfig()); - user.setIdp(this); + user.setName(firstName + lastName); + user.setIdpConfig(getConfig()); + user.setIdp(this); - return user; - } catch (Exception e) { - throw new IdentityBrokerException("Could not obtain user profile from facebook.", e); - } - } + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); - @Override - protected String getDefaultScopes() { - return DEFAULT_SCOPE; - } + return user; + } catch (Exception e) { + throw new IdentityBrokerException("Could not obtain user profile from facebook.", e); + } + } + + @Override + protected String getDefaultScopes() { + return DEFAULT_SCOPE; + } } diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUserAttributeMapper.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUserAttributeMapper.java new file mode 100644 index 0000000000..5a496573bb --- /dev/null +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookUserAttributeMapper.java @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.social.facebook; + +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; + +/** + * User attribute mapper. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class FacebookUserAttributeMapper extends AbstractJsonUserAttributeMapper { + + private static final String[] cp = new String[] { FacebookIdentityProviderFactory.PROVIDER_ID }; + + @Override + public String[] getCompatibleProviders() { + return cp; + } + + @Override + public String getId() { + return "facebook-user-attribute-mapper"; + } + +} diff --git a/social/facebook/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/social/facebook/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100755 index 0000000000..7e8f8aa9b6 --- /dev/null +++ b/social/facebook/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.keycloak.social.facebook.FacebookUserAttributeMapper \ No newline at end of file diff --git a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index cd36006976..b89d3b9c59 100755 --- a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -41,7 +41,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple user.setIdpConfig(getConfig()); user.setIdp(this); - AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile); + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); return user; } catch (Exception e) { diff --git a/social/google/src/main/java/org/keycloak/social/google/GoogleUserAttributeMapper.java b/social/google/src/main/java/org/keycloak/social/google/GoogleUserAttributeMapper.java new file mode 100644 index 0000000000..a2e7ef2946 --- /dev/null +++ b/social/google/src/main/java/org/keycloak/social/google/GoogleUserAttributeMapper.java @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.social.google; + +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; + +/** + * User attribute mapper. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class GoogleUserAttributeMapper extends AbstractJsonUserAttributeMapper { + + private static final String[] cp = new String[] { GoogleIdentityProviderFactory.PROVIDER_ID }; + + @Override + public String[] getCompatibleProviders() { + return cp; + } + + @Override + public String getId() { + return "google-user-attribute-mapper"; + } + +} diff --git a/social/google/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/social/google/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100755 index 0000000000..f0a3d86a67 --- /dev/null +++ b/social/google/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.keycloak.social.google.GoogleUserAttributeMapper \ No newline at end of file diff --git a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java index d6b7108635..2f439c2135 100755 --- a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java +++ b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java @@ -25,10 +25,11 @@ import org.codehaus.jackson.JsonNode; import org.jboss.logging.Logger; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.util.JsonSimpleHttp; -import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.social.SocialIdentityProvider; /** @@ -58,16 +59,18 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp try { JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken)); - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id")); + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id")); - String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl")); - user.setUsername(username); + String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl")); + user.setUsername(username); user.setName(getJsonProperty(profile, "formattedName")); user.setEmail(getJsonProperty(profile, "emailAddress")); - user.setIdpConfig(getConfig()); - user.setIdp(this); + user.setIdpConfig(getConfig()); + user.setIdp(this); - return user; + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); + + return user; } catch (Exception e) { throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e); } diff --git a/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInUserAttributeMapper.java b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInUserAttributeMapper.java new file mode 100644 index 0000000000..9bc89e7e50 --- /dev/null +++ b/social/linkedin/src/main/java/org/keycloak/social/linkedin/LinkedInUserAttributeMapper.java @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.social.linkedin; + +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; + +/** + * User attribute mapper. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class LinkedInUserAttributeMapper extends AbstractJsonUserAttributeMapper { + + private static final String[] cp = new String[] { LinkedInIdentityProviderFactory.PROVIDER_ID }; + + @Override + public String[] getCompatibleProviders() { + return cp; + } + + @Override + public String getId() { + return "linkedin-user-attribute-mapper"; + } + +} diff --git a/social/linkedin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/social/linkedin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100755 index 0000000000..61b7730c0b --- /dev/null +++ b/social/linkedin/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.keycloak.social.linkedin.LinkedInUserAttributeMapper \ No newline at end of file diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java index 280753bf21..ab9b97a609 100755 --- a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java +++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java @@ -26,10 +26,11 @@ import java.util.HashMap; import org.codehaus.jackson.JsonNode; import org.jboss.logging.Logger; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; import org.keycloak.broker.oidc.util.JsonSimpleHttp; -import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.social.SocialIdentityProvider; /** @@ -37,8 +38,7 @@ import org.keycloak.social.SocialIdentityProvider; * * @author Vlastimil Elias (velias at redhat dot com) */ -public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvider - implements SocialIdentityProvider { +public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { private static final Logger log = Logger.getLogger(StackoverflowIdentityProvider.class); @@ -54,8 +54,6 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide config.setUserInfoUrl(PROFILE_URL); } - - @Override protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { log.debug("doGetFederatedIdentity()"); @@ -67,18 +65,19 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide } JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(URL)).get("items").get(0); - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id")); + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id")); - String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link")); - user.setUsername(username); + String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link")); + user.setUsername(username); user.setName(unescapeHtml3(getJsonProperty(profile, "display_name"))); // email is not provided // user.setEmail(getJsonProperty(profile, "email")); - user.setIdpConfig(getConfig()); - user.setIdp(this); + user.setIdpConfig(getConfig()); + user.setIdp(this); + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); - return user; + return user; } catch (Exception e) { throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e); } diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowUserAttributeMapper.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowUserAttributeMapper.java new file mode 100644 index 0000000000..5fe3b97cbd --- /dev/null +++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowUserAttributeMapper.java @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + */ +package org.keycloak.social.stackoverflow; + +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; + +/** + * User attribute mapper. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class StackoverflowUserAttributeMapper extends AbstractJsonUserAttributeMapper { + + private static final String[] cp = new String[] { StackoverflowIdentityProviderFactory.PROVIDER_ID }; + + @Override + public String[] getCompatibleProviders() { + return cp; + } + + @Override + public String getId() { + return "stackoverflow-user-attribute-mapper"; + } + +} diff --git a/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100755 index 0000000000..b7a3a5e322 --- /dev/null +++ b/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.keycloak.social.stackoverflow.StackoverflowUserAttributeMapper \ No newline at end of file