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];