Skip to content

Commit

Permalink
KEYCLOAK-1487 Multivalued support for UserAttribute protocol mapper. …
Browse files Browse the repository at this point in the history
…End-to-end LDAP example test including application
  • Loading branch information
mposolda committed Jun 29, 2015
1 parent 09994d1 commit 605c88a
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 6 deletions.
Expand Up @@ -60,6 +60,7 @@ public class LDAPConstants {
public static final String SAM_ACCOUNT_NAME = "sAMAccountName";
public static final String EMAIL = "mail";
public static final String POSTAL_CODE = "postalCode";
public static final String STREET = "street";
public static final String MEMBER = "member";
public static final String MEMBER_OF = "memberOf";
public static final String OBJECT_CLASS = "objectclass";
Expand Down
Expand Up @@ -15,12 +15,15 @@
public class ProtocolMapperUtils {
public static final String USER_ATTRIBUTE = "user.attribute";
public static final String USER_SESSION_NOTE = "user.session.note";
public static final String MULTIVALUED = "multivalued";
public static final String USER_MODEL_PROPERTY_LABEL = "User Property";
public static final String USER_MODEL_PROPERTY_HELP_TEXT = "Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method.";
public static final String USER_MODEL_ATTRIBUTE_LABEL = "User Attribute";
public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map.";
public static final String USER_SESSION_MODEL_NOTE_LABEL = "User Session Note";
public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "Name of stored user session note within the UserSessionModel.note map.";
public static final String MULTIVALUED_LABEL = "Multivalued";
public static final String MULTIVALUED_HELP_TEXT = "Indicates if attribute supports multiple values. If true, then the list of all values of this attribute will be set as claim. If false, then just first value will be set as claim";

public static String getUserModelValue(UserModel user, String propertyName) {

Expand Down
@@ -1,5 +1,6 @@
package org.keycloak.protocol.oidc.mappers;

import org.jboss.logging.Logger;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapper;
Expand All @@ -19,6 +20,8 @@
* @version $Revision: 1 $
*/
public class OIDCAttributeMapperHelper {
private static final Logger logger = Logger.getLogger(OIDCAttributeMapperHelper.class);

public static final String TOKEN_CLAIM_NAME = "claim.name";
public static final String TOKEN_CLAIM_NAME_LABEL = "Token Claim Name";
public static final String JSON_TYPE = "Claim JSON Type";
Expand All @@ -31,6 +34,26 @@ public class OIDCAttributeMapperHelper {

public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object attributeValue) {
if (attributeValue == null) return null;

if (attributeValue instanceof List) {
List<Object> valueAsList = (List<Object>) attributeValue;
if (valueAsList.size() == 0) return null;

if (isMultivalued(mappingModel)) {
List<Object> result = new ArrayList<>();
for (Object valueItem : valueAsList) {
result.add(mapAttributeValue(mappingModel, valueItem));
}
return result;
} else {
if (valueAsList.size() > 1) {
logger.warnf("Multiple values found '%s' for protocol mapper '%s' but expected just single value", attributeValue.toString(), mappingModel.getName());
}

attributeValue = valueAsList.get(0);
}
}

String type = mappingModel.getConfig().get(JSON_TYPE);
if (type == null) return attributeValue;
if (type.equals("boolean")) {
Expand All @@ -53,8 +76,9 @@ public static Object mapAttributeValue(ProtocolMapperModel mappingModel, Object
}

public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
if (attributeValue == null) return;
attributeValue = mapAttributeValue(mappingModel, attributeValue);
if (attributeValue == null) return;

String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
String[] split = protocolClaim.split("\\.");
Map<String, Object> jsonObject = token.getOtherClaims();
Expand Down Expand Up @@ -102,6 +126,11 @@ public static boolean includeInAccessToken(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_ACCESS_TOKEN));
}


public static boolean isMultivalued(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get(ProtocolMapperUtils.MULTIVALUED));
}

public static void addAttributeConfig(List<ProviderConfigProperty> configProperties) {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
Expand Down
@@ -1,5 +1,6 @@
package org.keycloak.protocol.oidc.mappers;

import org.jboss.logging.Logger;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
Expand Down Expand Up @@ -35,6 +36,13 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
configProperties.add(property);
OIDCAttributeMapperHelper.addAttributeConfig(configProperties);

property = new ProviderConfigProperty();
property.setName(ProtocolMapperUtils.MULTIVALUED);
property.setLabel(ProtocolMapperUtils.MULTIVALUED_LABEL);
property.setHelpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT);
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
configProperties.add(property);

}

public static final String PROVIDER_ID = "oidc-usermodel-attribute-mapper";
Expand Down Expand Up @@ -76,7 +84,7 @@ public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel m
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
UserModel user = userSession.getUser();
String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
String attributeValue = user.getFirstAttribute(attributeName);
List<String> attributeValue = user.getAttribute(attributeName);
if (attributeValue == null) return;
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
}
Expand All @@ -92,12 +100,18 @@ public static ProtocolMapperModel createClaimMapper(String name,
String userAttribute,
String tokenClaimName, String claimType,
boolean consentRequired, String consentText,
boolean accessToken, boolean idToken) {
return OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
boolean accessToken, boolean idToken, boolean multivalued) {
ProtocolMapperModel mapper = OIDCAttributeMapperHelper.createClaimMapper(name, userAttribute,
tokenClaimName, claimType,
consentRequired, consentText,
accessToken, idToken,
PROVIDER_ID);

if (multivalued) {
mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true");
}

return mapper;
}


Expand Down
@@ -0,0 +1,58 @@
package org.keycloak.testsuite.federation;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.IDToken;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPExampleServlet extends HttpServlet {


@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
IDToken idToken = securityContext.getIdToken();

PrintWriter out = resp.getWriter();
out.println("<html><head><title>LDAP Portal</title></head><body>");
out.println("<table border><tr><th>Attribute name</th><th>Attribute values</th></tr>");

out.printf("<tr><td>%s</td><td>%s</td></tr>", "preferred_username", idToken.getPreferredUsername());
out.println();
out.printf("<tr><td>%s</td><td>%s</td></tr>", "name", idToken.getName());
out.println();
out.printf("<tr><td>%s</td><td>%s</td></tr>", "email", idToken.getEmail());
out.println();

for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
Object value = claim.getValue();

if (value instanceof List) {
List<String> asList = (List<String>) value;
StringBuilder result = new StringBuilder();
for (String item : asList) {
result.append(item + "<br>");
}
value = result.toString();
}

out.printf("<tr><td>%s</td><td>%s</td></tr>", claim.getKey(), value);
out.println();
}

out.println("</table></body></html>");
out.flush();
}

}
@@ -1,33 +1,50 @@
package org.keycloak.testsuite.federation;

import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.ws.rs.core.UriBuilder;

import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;
import org.junit.runners.MethodSorters;
import org.keycloak.OAuth2Constants;
import org.keycloak.federation.ldap.LDAPFederationProvider;
import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProvider;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.adapter.AdapterTest;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.LDAPRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPMultipleAttributesTest {

protected String APP_SERVER_BASE_URL = "http://localhost:8081";
protected String LOGIN_URL = OIDCLoginProtocolService.authUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth")).build("test").toString();

private static LDAPRule ldapRule = new LDAPRule();

private static UserFederationProviderModel ldapModel = null;
Expand All @@ -41,6 +58,24 @@ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmMod

ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", -1, -1, 0);
FederationTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
FederationTestUtils.addUserAttributeMapper(appRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET);

// Create ldap-portal client
ClientModel ldapClient = appRealm.addClient("ldap-portal");
ldapClient.addRedirectUri("/ldap-portal");
ldapClient.addRedirectUri("/ldap-portal/*");
ldapClient.setManagementUrl("/ldap-portal");
ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("postalCode", "postal_code", "postal_code", "String", true, "", true, true, true));
ldapClient.addProtocolMapper(UserAttributeMapper.createClaimMapper("street", "street", "street", "String", true, "", true, true, false));
ldapClient.addScopeMapping(appRealm.getRole("user"));
ldapClient.setSecret("password");

// Deploy ldap-portal client
URL url = getClass().getResource("/ldap/ldap-app-keycloak.json");
keycloakRule.createApplicationDeployment()
.name("ldap-portal").contextPath("/ldap-portal")
.servletClass(LDAPExampleServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplication();
}
});

Expand All @@ -49,6 +84,18 @@ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmMod
.outerRule(ldapRule)
.around(keycloakRule);

@Rule
public WebRule webRule = new WebRule(this);

@WebResource
protected WebDriver driver;

@WebResource
protected OAuthClient oauth;

@WebResource
protected LoginPage loginPage;

@Test
public void testModel() {
KeycloakSession session = keycloakRule.startSession();
Expand Down Expand Up @@ -105,6 +152,40 @@ private void assertPostalCodes(List<String> postalCodes, String... expectedPosta
}
}

@Test
public void ldapPortalEndToEndTest() {
// Login as bwilson
driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
loginPage.login("bwilson", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
String pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("bwilson") && pageSource.contains("Bruce"));
Assert.assertTrue(pageSource.contains("street") && pageSource.contains("Elm 5"));
Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441") && pageSource.contains("77332"));

// Logout
String logoutUri = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(APP_SERVER_BASE_URL + "/auth"))
.queryParam(OAuth2Constants.REDIRECT_URI, APP_SERVER_BASE_URL + "/ldap-portal").build("test").toString();
driver.navigate().to(logoutUri);

// Login as jbrown
driver.navigate().to(APP_SERVER_BASE_URL + "/ldap-portal");
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
loginPage.login("jbrown", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(APP_SERVER_BASE_URL + "/ldap-portal"));
pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("jbrown") && pageSource.contains("James Brown"));
Assert.assertFalse(pageSource.contains("street"));
Assert.assertTrue(pageSource.contains("postal_code") && pageSource.contains("88441"));
Assert.assertFalse(pageSource.contains("77332"));

// Logout
driver.navigate().to(logoutUri);
}



}
Expand Down
Expand Up @@ -69,7 +69,9 @@
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.hamcrest.Matchers.*;
Expand Down Expand Up @@ -622,13 +624,16 @@ public void testTokenMapping() throws Exception {
user.setSingleAttribute("postal_code", "02115");
user.setSingleAttribute("country", "USA");
user.setSingleAttribute("phone", "617-777-6666");
List<String> departments = Arrays.asList("finance", "development");
user.setAttribute("departments", departments);
ClientModel app = realm.getClientByClientId("test-app");
ProtocolMapperModel mapper = AddressMapper.createAddressMapper(true, true);
app.addProtocolMapper(mapper);
app.addProtocolMapper(HardcodedClaim.create("hard", "hard", "coded", "String", false, null, true, true));
app.addProtocolMapper(HardcodedClaim.create("hard-nested", "nested.hard", "coded-nested", "String", false, null, true, true));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("custom phone", "phone", "home_phone", "String", true, "", true, true, false));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true, false));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("departments", "departments", "department", "String", true, "", true, true, true));
app.addProtocolMapper(HardcodedRole.create("hard-realm", "hardcoded"));
app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded"));
app.addProtocolMapper(RoleNameMapper.create("rename-app-role", "test-app.customer-user", "realm-user"));
Expand All @@ -655,6 +660,9 @@ public void testTokenMapping() throws Exception {
Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)idToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone"));
List<String> departments = (List<String>)idToken.getOtherClaims().get("department");
Assert.assertEquals(2, departments.size());
Assert.assertTrue(departments.contains("finance") && departments.contains("development"));

AccessToken accessToken = getAccessToken(tokenResponse);
Assert.assertEquals(accessToken.getName(), "Tom Brady");
Expand All @@ -671,6 +679,9 @@ public void testTokenMapping() throws Exception {
Assert.assertEquals("coded-nested", nested.get("hard"));
nested = (Map)accessToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone"));
departments = (List<String>)idToken.getOtherClaims().get("department");
Assert.assertEquals(2, departments.size());
Assert.assertTrue(departments.contains("finance") && departments.contains("development"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user"));
Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user"));
Expand Down
@@ -0,0 +1,10 @@
{
"realm": "test",
"resource": "ldap-portal",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "external",
"credentials": {
"secret": "password"
}
}

0 comments on commit 605c88a

Please sign in to comment.