From 236b2b92731d88a0d6acdf4e98c8973bd32d9c3c Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Tue, 7 Mar 2017 00:39:32 +0100 Subject: [PATCH] KEYCLOAK-3599 Add Script based OIDC ProtocolMapper --- .../ScriptBasedOIDCProtocolMapper.java | 139 ++++++++++++++++++ .../org.keycloak.protocol.ProtocolMapper | 2 +- .../oauth/OIDCProtocolMappersTest.java | 4 + .../testsuite/util/ProtocolMapperUtil.java | 18 ++- .../admin/resources/js/controllers/clients.js | 23 ++- 5 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java new file mode 100644 index 0000000000..8b5025f1a8 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2017 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.protocol.oidc.mappers; + +import org.jboss.logging.Logger; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.representations.IDToken; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import java.util.List; + +/** + * OIDC {@link org.keycloak.protocol.ProtocolMapper} that uses a provided JavaScript fragment to compute the token claim value. + * + * @author Thomas Darimont + */ +public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { + + public static final String PROVIDER_ID = "oidc-script-based-protocol-mapper"; + + private static final Logger LOGGER = Logger.getLogger(ScriptBasedOIDCProtocolMapper.class); + + private static final String SCRIPT = "script"; + + private static final List configProperties; + + static { + + configProperties = ProviderConfigurationBuilder.create() + .property() + .name(SCRIPT) + .type(ProviderConfigProperty.SCRIPT_TYPE) + .label("Script") + .helpText( + "Script to compute the claim value. \n" + // + " Available variables: \n" + // + " 'user' - the current user.\n" + // + " 'realm' - the current realm.\n" + // + " 'token' - the current token.\n" + // + " 'userSession' - the current userSession.\n" // + ) + .defaultValue("/**\n" + // + " * Available variables: \n" + // + " * user - the current user\n" + // + " * realm - the current realm\n" + // + " * token - the current token\n" + // + " * userSession - the current userSession\n" + // + " */\n\n\n//insert your code here..." // + ) + .add() + .build(); + + OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserPropertyMapper.class); + } + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "Script Mapper"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Evaluates a javascript function to produce a token claim based on context information."; + } + + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { + + UserModel user = userSession.getUser(); + String script = mappingModel.getConfig().get(SCRIPT); + RealmModel realm = userSession.getRealm(); + + ScriptEngineManager engineManager = new ScriptEngineManager(); + ScriptEngine scriptEngine = engineManager.getEngineByName("javascript"); + + Bindings bindings = scriptEngine.createBindings(); + bindings.put("user", user); + bindings.put("realm", realm); + bindings.put("token", token); + bindings.put("userSession", userSession); + + Object claimValue; + try { + claimValue = scriptEngine.eval(script, bindings); + } catch (Exception ex) { + LOGGER.error("Error during execution of ProtocolMapper script", ex); + claimValue = null; + } + + OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claimValue); + } + + 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, + tokenClaimName, claimType, + consentRequired, consentText, + accessToken, idToken, + PROVIDER_ID); + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 95b79cfb01..a0f5627e06 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -36,4 +36,4 @@ org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper - +org.keycloak.protocol.oidc.mappers.ScriptBasedOIDCProtocolMapper diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index ad16c2d06f..d281196642 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java @@ -67,6 +67,7 @@ import static org.keycloak.testsuite.util.ProtocolMapperUtil.createClaimMapper; import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim; import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedRole; import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper; +import static org.keycloak.testsuite.util.ProtocolMapperUtil.createScriptMapper; /** * @author Marek Posolda @@ -146,6 +147,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { app.getProtocolMappers().createMapper(createHardcodedRole("hard-realm", "hardcoded")).close(); app.getProtocolMappers().createMapper(createHardcodedRole("hard-app", "app.hardcoded")).close(); app.getProtocolMappers().createMapper(createRoleNameMapper("rename-app-role", "test-app.customer-user", "realm-user")).close(); + app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper","computed-via-script", "computed-via-script", "String", true, true, "'hello_' + user.username")).close(); } { @@ -199,6 +201,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user")); assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded")); + assertEquals("hello_test-user@localhost", accessToken.getOtherClaims().get("computed-via-script")); oauth.openLogout(); } @@ -217,6 +220,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { || model.getName().equals("rename-app-role") || model.getName().equals("hard-realm") || model.getName().equals("hard-app") + || model.getName().equals("test-script-mapper") ) { app.getProtocolMappers().delete(model.getId()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java index 922f1ef07e..4925abcaf7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java @@ -1,12 +1,13 @@ package org.keycloak.testsuite.util; import org.keycloak.admin.client.resource.ProtocolMappersResource; +import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.utils.ModelToRepresentation; -import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AddressMapper; import org.keycloak.protocol.oidc.mappers.HardcodedClaim; import org.keycloak.protocol.oidc.mappers.HardcodedRole; import org.keycloak.protocol.oidc.mappers.RoleNameMapper; +import org.keycloak.protocol.oidc.mappers.ScriptBasedOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper; import org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper; @@ -149,4 +150,19 @@ public class ProtocolMapperUtil { } return null; } + + public static ProtocolMapperRepresentation createScriptMapper(String name, + String userAttribute, + String tokenClaimName, + String claimType, + boolean accessToken, + boolean idToken, + String script) { + + ProtocolMapperModel mapper = ScriptBasedOIDCProtocolMapper.createClaimMapper(name, userAttribute, tokenClaimName, claimType, false, null, accessToken, idToken); + mapper.getConfig().put("script", script); + + return ModelToRepresentation.toRepresentation(mapper); + } + } diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 76c9a9b701..279482f4c3 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -1779,11 +1779,11 @@ module.controller('ClientProtocolMapperCtrl', function($scope, realm, serverInfo protocol: client.protocol, mapper: angular.copy(mapper), changed: false - } + }; var protocolMappers = serverInfo.protocolMapperTypes[client.protocol]; for (var i = 0; i < protocolMappers.length; i++) { - if (protocolMappers[i].id == mapper.protocolMapper) { + if (protocolMappers[i].id === mapper.protocolMapper) { $scope.model.mapperType = protocolMappers[i]; } } @@ -1856,7 +1856,24 @@ module.controller('ClientProtocolMapperCreateCtrl', function($scope, realm, serv mapper: { protocol : client.protocol, config: {}}, changed: false, mapperTypes: serverInfo.protocolMapperTypes[protocol] - } + }; + + // apply default configurations on change for selected protocolmapper type. + $scope.$watch('model.mapperType', function() { + var currentMapperType = $scope.model.mapperType; + var defaultConfig = {}; + + if (currentMapperType && Array.isArray(currentMapperType.properties)) { + for (var i = 0; i < currentMapperType.properties.length; i++) { + var property = currentMapperType.properties[i]; + if (property && property.name && property.defaultValue) { + defaultConfig[property.name] = property.defaultValue; + } + } + } + + $scope.model.mapper.config = defaultConfig; + }, true); $scope.model.mapperType = $scope.model.mapperTypes[0];