From 74ace4006c67c3c87fe32c48c4f0efc99d8a9462 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 8 Jun 2015 17:04:53 +0200 Subject: [PATCH 1/6] Added base abstract implementation of Attribute mapper for Social providers, used for GitHub provider --- .../AbstractJsonUserAttributeMapper.java | 133 ++++++++++++++++++ .../social/github/GitHubIdentityProvider.java | 65 +++++---- .../github/GitHubUserAttributeMapper.java | 29 ++++ ...oak.broker.provider.IdentityProviderMapper | 1 + 4 files changed, 197 insertions(+), 31 deletions(-) create mode 100755 broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java create mode 100644 social/github/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java create mode 100755 social/github/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java new file mode 100755 index 0000000000..7f3817b1c4 --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java @@ -0,0 +1,133 @@ +package org.keycloak.broker.oidc.mappers; + +import java.util.ArrayList; +import java.util.List; + +import org.codehaus.jackson.JsonNode; +import org.jboss.logging.Logger; +import org.keycloak.broker.oidc.OIDCIdentityProvider; +import org.keycloak.broker.provider.AbstractIdentityProviderMapper; +import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user attribute. + * Concrete mapper classes with own ID and provider mapping must be implemented for each social provider who uses {@link JsonNode} user profile. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper { + + protected static final Logger logger = Logger.getLogger(AbstractJsonUserAttributeMapper.class); + + protected static final Logger LOGGER_DUMP_USER_PROFILE = Logger.getLogger("org.keycloak.social.user_profile_dump"); + + /** + * 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"; + + /** + * Key in {@link BrokeredIdentityContext#getContextData()} where {@link JsonNode} with user profile is stored. + */ + public static final String CONTEXT_JSON_NODE = OIDCIdentityProvider.USER_INFO; + + private static final List configProperties = new ArrayList(); + + 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); + } + + 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); + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getDisplayCategory() { + return "Attribute Importer"; + } + + @Override + public String getDisplayType() { + return "Attribute Importer"; + } + + @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 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; + } + + String value = getJsonValue(mapperModel, context); + if (value != null) { + user.setAttribute(attribute, value); + } + } + + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + // we do not update user profile from social provider + } + + protected static String getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + + String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD); + if (jsonField == null) { + logger.debug("JSON field is not configured"); + return null; + } + + JsonNode profileJsonNode = (JsonNode) context.getContextData().get(CONTEXT_JSON_NODE); + + 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."); + } + + return null; + } + +} 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 40a883b24a..cd36006976 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 @@ -3,10 +3,11 @@ package org.keycloak.social.github; 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,40 +15,42 @@ import org.keycloak.social.SocialIdentityProvider; */ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider implements SocialIdentityProvider { - public static final String AUTH_URL = "https://github.com/login/oauth/authorize"; - public static final String TOKEN_URL = "https://github.com/login/oauth/access_token"; - public static final String PROFILE_URL = "https://api.github.com/user"; - public static final String DEFAULT_SCOPE = "user:email"; + public static final String AUTH_URL = "https://github.com/login/oauth/authorize"; + public static final String TOKEN_URL = "https://github.com/login/oauth/access_token"; + public static final String PROFILE_URL = "https://api.github.com/user"; + public static final String DEFAULT_SCOPE = "user:email"; - public GitHubIdentityProvider(OAuth2IdentityProviderConfig config) { - super(config); - config.setAuthorizationUrl(AUTH_URL); - config.setTokenUrl(TOKEN_URL); - config.setUserInfoUrl(PROFILE_URL); - } + public GitHubIdentityProvider(OAuth2IdentityProviderConfig config) { + super(config); + config.setAuthorizationUrl(AUTH_URL); + config.setTokenUrl(TOKEN_URL); + config.setUserInfoUrl(PROFILE_URL); + } - @Override - protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { - try { - JsonNode profile = JsonSimpleHttp.asJson(SimpleHttp.doGet(PROFILE_URL).header("Authorization", "Bearer " + accessToken)); + @Override + protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { + 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 = getJsonProperty(profile, "login"); - user.setUsername(username); - user.setName(getJsonProperty(profile, "name")); - user.setEmail(getJsonProperty(profile, "email")); - user.setIdpConfig(getConfig()); - user.setIdp(this); + String username = getJsonProperty(profile, "login"); + user.setUsername(username); + user.setName(getJsonProperty(profile, "name")); + user.setEmail(getJsonProperty(profile, "email")); + user.setIdpConfig(getConfig()); + user.setIdp(this); - return user; - } catch (Exception e) { - throw new IdentityBrokerException("Could not obtain user profile from github.", e); - } - } + AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile); - @Override - protected String getDefaultScopes() { - return DEFAULT_SCOPE; - } + return user; + } catch (Exception e) { + throw new IdentityBrokerException("Could not obtain user profile from github.", e); + } + } + + @Override + protected String getDefaultScopes() { + return DEFAULT_SCOPE; + } } diff --git a/social/github/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java b/social/github/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java new file mode 100644 index 0000000000..b4a6359076 --- /dev/null +++ b/social/github/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.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.github; + +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; + +/** + * User attribute mapper. + * + * @author Vlastimil Elias (velias at redhat dot com) + */ +public class GitHubUserAttributeMapper extends AbstractJsonUserAttributeMapper { + + private static final String[] cp = new String[] { GitHubIdentityProviderFactory.PROVIDER_ID }; + + @Override + public String[] getCompatibleProviders() { + return cp; + } + + @Override + public String getId() { + return "github-user-attribute-mapper"; + } + +} diff --git a/social/github/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/social/github/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper new file mode 100755 index 0000000000..25972f6fa3 --- /dev/null +++ b/social/github/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper @@ -0,0 +1 @@ +org.keycloak.social.github.GitHubUserAttributeMapper \ No newline at end of file From c6e0195a3c67246c0a71209ec48f2cc629623d3e Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Tue, 9 Jun 2015 13:49:58 +0200 Subject: [PATCH 2/6] 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 From 1ba5ea411b614e398dae936452db6858829f2534 Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Wed, 10 Jun 2015 16:14:19 +0530 Subject: [PATCH 3/6] [KEYCLOAK-1424] Login button on login screen should be on the left --- .../resources/theme/keycloak/login/resources/css/login.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css index 18d97e09a3..b64bdc70da 100644 --- a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css +++ b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css @@ -121,11 +121,6 @@ display: block; } -#kc-login { - float: right; - margin-left: 10px; -} - #kc-feedback-wrapper { display: inline-block; width: auto; From 30405804bcb571e427d395787835b1484c1c2108 Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Wed, 10 Jun 2015 17:42:22 +0530 Subject: [PATCH 4/6] [KEYCLOAK-1425] Remove address options on registration screen --- .../resources/theme/base/login/register.ftl | 47 ------------------- .../FederationProvidersIntegrationTest.java | 4 +- .../testsuite/pages/RegisterPage.java | 19 -------- 3 files changed, 1 insertion(+), 69 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/login/register.ftl b/forms/common-themes/src/main/resources/theme/base/login/register.ftl index 63d8c22098..aadd022cce 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/register.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/register.ftl @@ -61,53 +61,6 @@ -
-
- -
- -
- -
-
-
-
- -
- -
- -
-
-
-
- -
- -
- -
-
-
-
- -
- -
- -
-
-
-
- -
- -
- -
-
- -
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java index c384651fcd..4ed0d28372 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationProvidersIntegrationTest.java @@ -247,7 +247,7 @@ public class FederationProvidersIntegrationTest { loginPage.clickRegister(); registerPage.assertCurrent(); - registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "Password1", "Password1", "non-LDAP-Mapped street", null, null, "78910", null); + registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "Password1", "Password1"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); KeycloakSession session = keycloakRule.startSession(); @@ -257,8 +257,6 @@ public class FederationProvidersIntegrationTest { Assert.assertNotNull(user); Assert.assertNotNull(user.getFederationLink()); Assert.assertEquals(user.getFederationLink(), ldapModel.getId()); - Assert.assertEquals("78910", user.getAttribute("postal_code")); - Assert.assertEquals("non-LDAP-Mapped street", user.getAttribute("street")); } finally { keycloakRule.stopSession(session, false); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java index 28c37963d1..5904da1a3a 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -57,25 +57,6 @@ public class RegisterPage extends AbstractPage { @FindBy(className = "feedback-error") private WebElement loginErrorMessage; - public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm, - String street, String cityOrLocality, String stateOrRegion, String zipOrPostalCode, String country) { - fillExtendedField("street", street); - fillExtendedField("locality", cityOrLocality); - fillExtendedField("region", stateOrRegion); - fillExtendedField("postal_code", zipOrPostalCode); - fillExtendedField("country", country); - - register(firstName, lastName, email, username, password, passwordConfirm); - } - - private void fillExtendedField(String fieldName, String value) { - WebElement field = driver.findElement(By.id("user.attributes." + fieldName)); - field.clear(); - if (value != null) { - field.sendKeys(value); - } - } - public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) { firstNameInput.clear(); if (firstName != null) { From 92efe8441e2f90ef4b8776fff8f0e2751645e712 Mon Sep 17 00:00:00 2001 From: girirajsharma Date: Wed, 10 Jun 2015 20:23:39 +0530 Subject: [PATCH 5/6] [KEYCLOAK-1431] Update page titles --- .../base/admin/resources/partials/brute-force.html | 2 +- .../resources/partials/client-clustering.html | 2 +- .../resources/partials/client-credentials.html | 2 +- .../admin/resources/partials/client-detail.html | 4 ++-- .../admin/resources/partials/client-import.html | 2 +- .../resources/partials/client-installation.html | 2 +- .../base/admin/resources/partials/client-keys.html | 2 +- .../base/admin/resources/partials/client-list.html | 2 +- .../resources/partials/client-mappers-add.html | 2 +- .../admin/resources/partials/client-mappers.html | 2 +- .../resources/partials/client-revocation.html | 2 +- .../resources/partials/client-role-detail.html | 4 ++-- .../admin/resources/partials/client-role-list.html | 2 +- .../resources/partials/client-saml-key-export.html | 2 +- .../resources/partials/client-saml-key-import.html | 2 +- .../admin/resources/partials/client-saml-keys.html | 2 +- .../resources/partials/client-scope-mappings.html | 2 +- .../admin/resources/partials/client-sessions.html | 2 +- .../admin/resources/partials/defense-headers.html | 2 +- .../resources/partials/federated-generic.html | 4 ++-- .../resources/partials/federated-kerberos.html | 4 ++-- .../admin/resources/partials/federated-ldap.html | 4 ++-- .../partials/federated-mapper-detail.html | 4 ++-- .../resources/partials/federated-mappers.html | 2 +- .../partials/identity-provider-mapper-detail.html | 6 +++--- .../partials/identity-provider-mappers.html | 2 +- .../resources/partials/protocol-mapper-detail.html | 4 ++-- .../resources/partials/realm-cache-settings.html | 2 +- .../resources/partials/realm-credentials.html | 2 +- .../resources/partials/realm-default-roles.html | 2 +- .../admin/resources/partials/realm-detail.html | 2 +- .../resources/partials/realm-events-admin.html | 2 +- .../resources/partials/realm-events-config.html | 4 ++-- .../admin/resources/partials/realm-events.html | 2 +- .../partials/realm-identity-provider-export.html | 4 ++-- .../partials/realm-identity-provider-oidc.html | 4 ++-- .../partials/realm-identity-provider-saml.html | 4 ++-- .../partials/realm-identity-provider-social.html | 4 ++-- .../realm-identity-provider-stackoverflow-ext.html | 14 +++++++------- .../partials/realm-identity-provider.html | 2 +- .../base/admin/resources/partials/realm-keys.html | 2 +- .../resources/partials/realm-login-settings.html | 2 +- .../base/admin/resources/partials/realm-smtp.html | 2 +- .../resources/partials/realm-theme-settings.html | 2 +- .../admin/resources/partials/realm-tokens.html | 2 +- .../base/admin/resources/partials/role-detail.html | 4 ++-- .../base/admin/resources/partials/role-list.html | 2 +- .../admin/resources/partials/role-mappings.html | 2 +- .../admin/resources/partials/session-realm.html | 2 +- .../resources/partials/session-revocation.html | 2 +- .../admin/resources/partials/user-consents.html | 2 +- .../admin/resources/partials/user-credentials.html | 2 +- .../base/admin/resources/partials/user-detail.html | 2 +- .../partials/user-federated-identity.html | 2 +- .../admin/resources/partials/user-federation.html | 2 +- .../base/admin/resources/partials/user-list.html | 2 +- .../admin/resources/partials/user-sessions.html | 2 +- 57 files changed, 78 insertions(+), 78 deletions(-) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html index ebaa6b4a89..cd1c7546bf 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/brute-force.html @@ -1,5 +1,5 @@
-

Settings {{realm.realm|capitalize}}

+

Settings

diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html index c805ccbea8..71e08501bf 100644 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-clustering.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html index cc6daae8da..efdc15d4ab 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-credentials.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index f3b4739272..e7e40962a7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -6,8 +6,8 @@
  • {{client.clientId}}
  • -

    Add Client

    -

    Client {{client.clientId|capitalize}}

    +

    Add Client

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-import.html index bba026b6ec..50cfc0ea76 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-import.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-import.html @@ -5,7 +5,7 @@
  • Import Client
  • -

    Import Client

    +

    Import Client

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html index e6dd11b608..55581c3ce0 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-installation.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html index e7fead233f..549b0565ff 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-keys.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html index 782c782732..36696f769c 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-list.html @@ -1,6 +1,6 @@

    - Clients {{realm.realm|capitalize}} + Clients Clients are trusted browser apps and web services in a realm. These clients can request a login. You can also define client specific roles.

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html index a7a3b11d1e..c3fda83833 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers-add.html @@ -7,7 +7,7 @@
  • Add Builtin Protocol Mappers
  • -

    Add Builtin Protocol Mapper

    +

    Add Builtin Protocol Mapper

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html index 0f1f27481e..1e418b4de7 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-mappers.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html index 38ce042096..522f554549 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-revocation.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html index 706c2d3cee..2c267ca122 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-detail.html @@ -8,8 +8,8 @@
  • {{role.name}}
  • -

    Add Role

    -

    Role {{role.name}}

    +

    Add Role

    +

    {{role.name|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html index e892d35b18..c9ec943773 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-role-list.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-export.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-export.html index 7d0facbe56..70a4c7d048 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-export.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-export.html @@ -7,7 +7,7 @@
  • SAML {{keyType}} Key Export
  • -

    Export SAML Key {{client.clientId|capitalize}}

    +

    Export SAML Key {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-import.html index 76ee50d12c..8ea421b864 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-import.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-key-import.html @@ -7,7 +7,7 @@
  • SAML {{keyType}} Key Import
  • -

    Import SAML Key {{client.clientId|capitalize}}

    +

    Import SAML Key {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html index 5f0f1ef7fe..74eb8401d4 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-saml-keys.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html index 5340685558..79eccd5aa8 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-scope-mappings.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html index f93670351a..838b166d55 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-sessions.html @@ -5,7 +5,7 @@
  • {{client.clientId}}
  • -

    Client {{client.clientId|capitalize}}

    +

    {{client.clientId|capitalize}}

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html index f7cda2793a..ca8511c208 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html @@ -1,5 +1,5 @@
    -

    Settings {{realm.realm|capitalize}}

    +

    Settings

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html index f0d8774f43..bff634aeb1 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-generic.html @@ -5,8 +5,8 @@
  • Add User Federation Provider
  • -

    {{instance.providerName|capitalize}} User Federation Provider {{instance.displayName|capitalize}}

    -

    Add {{instance.providerName|capitalize}} User Federation Provider

    +

    {{instance.providerName|capitalize}}

    +

    Add {{instance.providerName|capitalize}} User Federation Provide

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html index 4498988665..9a042e39a3 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html @@ -1,5 +1,5 @@
    -

    Users {{realm.realm|capitalize}}

    +

    Users

    diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html index ee472355a1..762dda9243 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-sessions.html @@ -4,7 +4,7 @@
  • {{user.username}}
  • -

    User {{user.username|capitalize}}

    +

    {{user.username|capitalize}}

    From 03b521926a444336b78aab7fb4c4db492416b110 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 10 Jun 2015 17:00:56 +0200 Subject: [PATCH 6/6] KEYCLOAK-1260 Fix saml backchannel logout with JPA UserSession provider on MySQL + PostgreSQL --- .../java/org/keycloak/protocol/saml/SamlService.java | 12 +++++++++++- .../SAMLKeyCloakServerBrokerWithSignatureTest.java | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 09b033b9d7..40ea8fdadb 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -374,11 +374,21 @@ public class SamlService { for (String sessionIndex : logoutRequest.getSessionIndex()) { ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex); if (clientSession == null) continue; + UserSessionModel userSession = clientSession.getUserSession(); if (clientSession.getClient().getClientId().equals(client.getClientId())) { // remove requesting client from logout clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); + + // Remove also other clientSessions of this client as there could be more in this UserSession + if (userSession != null) { + for (ClientSessionModel clientSession2 : userSession.getClientSessions()) { + if (clientSession2.getClient().getId().equals(client.getId())) { + clientSession2.setAction(ClientSessionModel.Action.LOGGED_OUT); + } + } + } } - UserSessionModel userSession = clientSession.getUserSession(); + try { authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true); } catch (Exception e) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index 34c10d5e3f..b94f4df193 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -45,6 +45,11 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractIdentityP } }; + // @Test + public void testSleep() throws Exception { + Thread.sleep(100000000); + } + @Override protected String getProviderId() { return "kc-saml-signed-idp";