From e2436c472297849bf6678a63219961cde3ef2d4d Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 24 Mar 2016 15:56:40 -0400 Subject: [PATCH] KEYCLOAK-2624 --- .../oidc/mappers/UserAttributeMapper.java | 34 ++- .../broker/OIDCBrokerUserPropertyTest.java | 213 ++++++++++++++++++ .../realm-with-oidc-property-mappers.json | 145 ++++++++++++ .../broker-test/test-realm-with-broker.json | 56 +++++ 4 files changed, 440 insertions(+), 8 deletions(-) create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java create mode 100755 testsuite/integration/src/test/resources/broker-test/realm-with-oidc-property-mappers.json diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java index 0d9154d881..3cd35d77a9 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java @@ -53,7 +53,7 @@ public class UserAttributeMapper extends AbstractClaimMapper { property = new ProviderConfigProperty(); property.setName(USER_ATTRIBUTE); property.setLabel("User Attribute Name"); - property.setHelpText("User attribute name to store claim."); + property.setHelpText("User attribute name to store claim. Use email, lastName, and firstName to map to those predefined user properties."); property.setType(ProviderConfigProperty.STRING_TYPE); configProperties.add(property); } @@ -90,7 +90,15 @@ public class UserAttributeMapper extends AbstractClaimMapper { String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); Object value = getClaimValue(mapperModel, context); if (value != null) { - context.setUserAttribute(attribute, value.toString()); + if (attribute.equalsIgnoreCase("email")) { + context.setEmail(value.toString()); + } else if (attribute.equalsIgnoreCase("firstName")) { + context.setFirstName(value.toString()); + } else if (attribute.equalsIgnoreCase("lastName")) { + context.setLastName(value.toString()); + } else { + context.setUserAttribute(attribute, value.toString()); + } } } @@ -98,17 +106,27 @@ public class UserAttributeMapper extends AbstractClaimMapper { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String attribute = mapperModel.getConfig().get(USER_ATTRIBUTE); Object value = getClaimValue(mapperModel, context); - String current = user.getFirstAttribute(attribute); - if (value != null && !value.equals(current)) { - user.setSingleAttribute(attribute, value.toString()); - } else if (value == null) { - user.removeAttribute(attribute); + String stringValue = null; + if (value != null) stringValue = value.toString(); + if (attribute.equalsIgnoreCase("email")) { + user.setEmail(stringValue); + } else if (attribute.equalsIgnoreCase("firstName")) { + user.setFirstName(stringValue); + } else if (attribute.equalsIgnoreCase("lastName")) { + user.setLastName(stringValue); + } else { + String current = user.getFirstAttribute(attribute); + if (stringValue != null && !stringValue.equals(current)) { + user.setSingleAttribute(attribute, stringValue); + } else if (value == null) { + user.removeAttribute(attribute); + } } } @Override public String getHelpText() { - return "Import declared claim if it exists in ID or access token into the specified user attribute."; + return "Import declared claim if it exists in ID or access token into the specified user property or attribute."; } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java new file mode 100755 index 0000000000..49b4426050 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCBrokerUserPropertyTest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.saml.processing.web.util.PostBindingUtil; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; + +import javax.mail.MessagingException; +import java.io.IOException; + +import static org.junit.Assert.*; + +/** + * Test that the broker AttributeMapper maps user properties like email, firstName, and lastName + * @author pedroigor + */ +public class OIDCBrokerUserPropertyTest extends AbstractKeycloakIdentityProviderTest { + + @ClassRule + public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { + + @Override + protected void configureServer(KeycloakServer server) { + server.getConfig().setPort(8082); + } + + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + server.importRealm(getClass().getResourceAsStream("/broker-test/realm-with-oidc-property-mappers.json")); + } + + @Override + protected String[] getTestRealms() { + return new String[] { "realm-with-oidc-idp-property-mappers" }; + } + }; + + @Override + protected String getProviderId() { + return "kc-oidc-idp-property-mappers"; + } + + @Override + protected void doAssertFederatedUser(UserModel federatedUser, IdentityProviderModel identityProviderModel, String expectedEmail, boolean isProfileUpdateExpected) { + if (isProfileUpdateExpected) { + super.doAssertFederatedUser(federatedUser, identityProviderModel, expectedEmail, isProfileUpdateExpected); + } else { + assertEquals(expectedEmail, federatedUser.getEmail()); + assertNotNull(federatedUser.getFirstName()); + assertNotNull(federatedUser.getLastName()); + } + } + + @Override + protected void doAssertFederatedUserNoEmail(UserModel federatedUser) { + assertEquals("kc-saml-idp-basic.test-user-noemail", federatedUser.getUsername()); + //assertEquals("", federatedUser.getEmail()); + assertEquals(null, federatedUser.getFirstName()); + assertEquals(null, federatedUser.getLastName()); + } + + @Override + protected void doAssertTokenRetrieval(String pageSource) { + try { + SAML2Request saml2Request = new SAML2Request(); + ResponseType responseType = (ResponseType) saml2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); + + assertNotNull(responseType); + assertFalse(responseType.getAssertions().isEmpty()); + } catch (Exception e) { + fail("Could not parse token."); + } + } + + @Override + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile() { + super.testSuccessfulAuthenticationWithoutUpdateProfile(); + } + + @Test + @Ignore + @Override + public void testSuccessfulAuthentication() { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationUpdateProfileOnMissing_nothingMissing() { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationUpdateProfileOnMissing_missingEmail() { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled() throws IOException, MessagingException { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailNotProvided_emailVerifyEnabled() { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled_emailTrustEnabled() { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthentication_emailTrustEnabled_emailVerifyEnabled_emailUpdatedOnFirstLogin() throws IOException, MessagingException { + // ignore + } + + @Override + @Ignore + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + // ignore + } + + @Override + @Ignore + @Test + public void testDisabled() { + // ignore + } + + @Override + @Test + @Ignore + public void testProviderOnLoginPage() { + // ignore + } + + @Override + @Test + @Ignore + public void testAccountManagementLinkIdentity() { + // ignore + } + + @Override + @Test + @Ignore + public void testAccountManagementLinkedIdentityAlreadyExists() { + // ignore + } + + @Override + @Test + @Ignore + public void testIdentityProviderNotAllowed() { + // ignore + } + + @Override + @Test + @Ignore + public void testTokenStorageAndRetrievalByApplication() { + // ignore + } + + @Override + @Test + @Ignore + public void testWithLinkedFederationProvider() throws Exception { + // ignore + } +} diff --git a/testsuite/integration/src/test/resources/broker-test/realm-with-oidc-property-mappers.json b/testsuite/integration/src/test/resources/broker-test/realm-with-oidc-property-mappers.json new file mode 100755 index 0000000000..f75bc450ac --- /dev/null +++ b/testsuite/integration/src/test/resources/broker-test/realm-with-oidc-property-mappers.json @@ -0,0 +1,145 @@ +{ + "id": "realm-with-oidc-idp-property-mappers", + "realm": "realm-with-oidc-idp-property-mappers", + "enabled": true, + "requiredCredentials": [ "password" ], + "defaultRoles": [ "foo", "bar" ], + "privateKey": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCCPyvTTb14vSMkpe/pds2P5Cqxk7bkeFnQiNMS1vyZ+HS2O79fxzp1eAguHnBTs4XTRT7SZJhIT/6utgqZjmDigKV5N7X5ptq8BM/W1qa1cYBRip261pc+tWf3IywJYQ9yFI9mUQarmIEl0D7GH16NSZklheaWfbodRVarvX+ML0amNtGYVDft/RftYmgbKKrK218qQp9R4GZFtf/Q/RmboNXN7weMINU8GWVkTRrccKBIXSunT6zXGfuj3Wp1YpVq20BWwY2OMM/P+yDAc7LKEO1LJqPBdT4r9BRn2lXiaga3AL24gTKZPKU/tu7uqfFciF+i4Rr58SMDNOzQcnklAgMBAAECggEAc0eibJYEO5d8QXW1kPgcHV2gBChv2mxDYnWYDLbIQSdNdfYP/qABt/MTmm5KkWr16fcCEYoD1w0mqFBrtVn1msSusUmEAYGTXJMNumOmjjX1kzaTQMmqeFBrwqwYz/xehWR5P+A7fSmwNV3KEeW19GvN5w5K96w0TLAQdFV3TQVPSytusDunwuR1yltMe1voaEDZ9z0Pi08YiEk2f6xhj5CMkoiw3mNImzfruphHullxU4FD05fH6tDeJ381527ILpAzDsgYZh4aFLKjUHem96bX4EL7FIzBJ6okgN78AZnUC/EaVfgFTw0qfhoWvZV4ruVXXiMhCg4CMMRDq/k9iQKBgQDBNWsJMT84OnnWmQoJmZogkFV+tsGrSK6Re+aJxLWpishh7dwAnT2OcagZvVdUb0FwNWu1D0B9/SKDDMRnnHBhOGDpH57m/eQdRU0oX1BD27xvffk0lLcfD4BTxnR5e9jss8K4twc9jf0P1rxC/loGJ2NtCH0BrPHgz54Ea+96ewKBgQCsk3JDaaPnFwzVYm2BXlhxOxLPsF4wvD2rIRAswZV4C5xebjand8nwiMmVpNd0PRLkEnkI+waURGv2EY/P3JsssoiY8Xqe8f/1G+SQKre7lbqOas8rFoALepC0BYDiZDFy0Z9ZnRAFzRI5sgIt7jpoMRD4xDNlmiV8X+yBxc3Y3wKBgQChDQsU1YUyNKQ8+sLAL9anEEkD4Ald4q8JPHN2IY+gLLxNzT0XEfsu0pTiJ8805axxgUYv3e/PVYNAJBNPnrqaf6lgiegl+jr9Hzhqz9CTUAYqFaL2boSakoxQyNtsLI0s+cb1vDN/3uy0GDZDzcty18BsMagqDmRtFgNNAj/UIwKBgQCahbeFBv0cOPZjxisY8Bou4N8aGehsqNBq/0LVYExuXa8YmoTTdJ3bgw9Er4G/ccQNdUDsuqAMeCtW/CiRzQ0ge4d1sprB4Rv3I4+HSsiS7SFKzfZLtWzXWlpg5qCdlWr1TR7qhYjIOPO9t1beO3YOvwhcRoliyyAPenBxTmTfbwKBgDtm2WJ5VlQgNpIdOs1CCiqd0DFmWOmvBPspPC1kySiy+Ndr9jNohRZkR7pEjgqA5E8rdzc88LirUN7bY5HFHRWN9KXrs5/o3O1K3GFCp64N6nvnPEYZ2zSJalcMC2fjSsJg26z8Dg1H+gfTIDUMoGiEAAnJXuqk+WayPU+fZMLn", + "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgj8r0029eL0jJKXv6XbNj+QqsZO25HhZ0IjTEtb8mfh0tju/X8c6dXgILh5wU7OF00U+0mSYSE/+rrYKmY5g4oCleTe1+abavATP1tamtXGAUYqdutaXPrVn9yMsCWEPchSPZlEGq5iBJdA+xh9ejUmZJYXmln26HUVWq71/jC9GpjbRmFQ37f0X7WJoGyiqyttfKkKfUeBmRbX/0P0Zm6DVze8HjCDVPBllZE0a3HCgSF0rp0+s1xn7o91qdWKVattAVsGNjjDPz/sgwHOyyhDtSyajwXU+K/QUZ9pV4moGtwC9uIEymTylP7bu7qnxXIhfouEa+fEjAzTs0HJ5JQIDAQAB", + "clients" : [ + { + "clientId": "broker-app", + "consentRequired": "false", + "enabled": true, + "fullScopeAllowed": true, + "secret": "secret", + "redirectUris": [ + "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-oidc-idp-property-mappers/endpoint/*" + ], + "protocolMappers": [ + { + "name": "mobile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "mobile", + "claim.name": "mobile", + "Claim JSON Type": "String", + "access.token.claim": "true", + "id.token.claim": "true" + + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "username", + "claim.name": "preferred_username", + "Claim JSON Type": "String", + "access.token.claim": "true", + "id.token.claim": "true" + + } + }, + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "claim.name": "email", + "Claim JSON Type": "String", + "access.token.claim": "true", + "id.token.claim": "true" + + } + }, + { + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "firstName", + "claim.name": "given_name", + "Claim JSON Type": "String", + "access.token.claim": "true", + "id.token.claim": "true" + + } + }, + { + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "lastName", + "claim.name": "family_name", + "Claim JSON Type": "String", + "access.token.claim": "true", + "id.token.claim": "true" + + } + } + ] + } + ], + "scopeMappings": [ + { + "client": "broker-app", + "roles": ["manager"] + } + ], + "users": [ + { + "username" : "test-user", + "enabled": true, + "email" : "test-user@localhost", + "firstName" : "Test", + "lastName" : "User", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"], + "attributes": { + "mobile": "617-666-7777" + } + }, + { + "username" : "test-user-noemail", + "enabled": true, + "firstName" : "Test", + "lastName" : "User", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"] + }, + { + "username" : "pedroigor", + "enabled": true, + "email" : "psilva@redhat.com", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["manager"] + } + ], + "roles" : { + "realm" : [ + { + "name": "manager", + "description": "Have Manager privileges" + } + ] + } +} \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json index 8717619721..84bcaf8ee5 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json +++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json @@ -186,9 +186,65 @@ "defaultScope": "email profile", "backchannelSupported": "true" } + }, + { + "alias" : "kc-oidc-idp-property-mappers", + "providerId" : "keycloak-oidc", + "enabled": true, + "storeToken" : true, + "addReadTokenRoleOnCreate": true, + "config": { + "clientId": "broker-app", + "clientSecret": "secret", + "prompt": "login", + "authorizationUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-property-mappers/protocol/openid-connect/auth", + "tokenUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-property-mappers/protocol/openid-connect/token", + "userInfoUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-property-mappers/protocol/openid-connect/userinfo", + "logoutUrl": "http://localhost:8082/auth/realms/realm-with-oidc-idp-property-mappers/protocol/openid-connect/logout", + "defaultScope": "email profile", + "backchannelSupported": "true" + } } ], "identityProviderMappers": [ + { + "name": "manager-mapper", + "identityProviderAlias": "kc-oidc-idp-property-mappers", + "identityProviderMapper": "oidc-role-idp-mapper", + "config": { + "role": "manager", + "claim": "realm_access.roles", + "claim.value": "manager" + } + + }, + { + "name": "email-mapper", + "identityProviderAlias": "kc-oidc-idp-property-mappers", + "identityProviderMapper": "oidc-user-attribute-idp-mapper", + "config": { + "user.attribute": "email", + "claim": "email" + } + }, + { + "name": "first-mapper", + "identityProviderAlias": "kc-oidc-idp-property-mappers", + "identityProviderMapper": "oidc-user-attribute-idp-mapper", + "config": { + "user.attribute": "firstName", + "claim": "given_name" + } + }, + { + "name": "last-mapper", + "identityProviderAlias": "kc-oidc-idp-property-mappers", + "identityProviderMapper": "oidc-user-attribute-idp-mapper", + "config": { + "user.attribute": "lastName", + "claim": "family_name" + } + }, { "name": "manager-mapper", "identityProviderAlias": "kc-oidc-idp",