From 8e113384aaedfd6c29934e3f4c3e55f460fcb8c9 Mon Sep 17 00:00:00 2001
From: Thomas Darimont
Date: Mon, 29 Aug 2016 18:20:13 +0200
Subject: [PATCH 1/9] KEYCLOAK-3491 Revise Scripting Support
Refactored the scripting infrastructure and added documentation.
Added tests and an authenticator template in JavaScript for a quickstart.
Increased height of ace code editor to 600px to avoid scrolling.
---
.../java/org/keycloak/models/ScriptModel.java | 23 ++-
.../keycloak/scripting/InvocableScript.java | 64 -------
.../scripting/InvocableScriptAdapter.java | 118 ++++++++++++
.../java/org/keycloak/scripting/Script.java | 18 ++
.../scripting/ScriptBindingsConfigurer.java | 20 +-
.../scripting/ScriptExecutionException.java | 20 +-
.../keycloak/scripting/ScriptingProvider.java | 35 +++-
.../browser/ScriptBasedAuthenticator.java | 144 ++++++++++----
.../ScriptBasedAuthenticatorFactory.java | 42 +++-
.../scripting/DefaultScriptingProvider.java | 91 ++++++---
.../DefaultScriptingProviderFactory.java | 28 ++-
.../scripts/authenticator-template.js | 37 ++++
.../admin/authentication/ProvidersTest.java | 2 +-
.../forms/ScriptAuthenticatorTest.java | 180 ++++++++++++++++++
.../scripts/authenticator-example.js | 10 +
.../admin/resources/js/controllers/realm.js | 15 +-
.../templates/kc-component-config.html | 2 +-
.../keycloak/admin/resources/css/styles.css | 2 +-
18 files changed, 693 insertions(+), 158 deletions(-)
delete mode 100644 server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java
create mode 100644 server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
create mode 100644 services/src/main/resources/scripts/authenticator-template.js
create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java
create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js
diff --git a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
index 8d6d5fd665..c4b9735030 100644
--- a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
@@ -1,12 +1,33 @@
+/*
+ * 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.models;
/**
- * Denotes an executable Script with metadata.
+ * A representation of a Script with some additional meta-data.
*
* @author Thomas Darimont
*/
public interface ScriptModel {
+ /**
+ * MIME-Type for JavaScript
+ */
+ String TEXT_JAVASCRIPT = "text/javascript";
+
/**
* Returns the unique id of the script. {@literal null} for ad-hoc created scripts.
*/
diff --git a/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java b/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java
deleted file mode 100644
index 342652fa17..0000000000
--- a/server-spi/src/main/java/org/keycloak/scripting/InvocableScript.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.keycloak.scripting;
-
-import org.keycloak.models.ScriptModel;
-
-import javax.script.Invocable;
-import javax.script.ScriptEngine;
-import javax.script.ScriptException;
-
-/**
- * @author Thomas Darimont
- */
-public class InvocableScript implements Invocable {
-
- /**
- * Holds the script metadata as well as the actual script.
- */
- private final ScriptModel script;
-
- /**
- * Holds the {@link ScriptEngine} instance initialized with the script code.
- */
- private final ScriptEngine scriptEngine;
-
- public InvocableScript(ScriptModel script, ScriptEngine scriptEngine) {
- this.script = script;
- this.scriptEngine = scriptEngine;
- }
-
- @Override
- public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptException, NoSuchMethodException {
- return getInvocableEngine().invokeMethod(thiz, name, args);
- }
-
- @Override
- public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
- return getInvocableEngine().invokeFunction(name, args);
- }
-
- @Override
- public T getInterface(Class clazz) {
- return getInvocableEngine().getInterface(clazz);
- }
-
- @Override
- public T getInterface(Object thiz, Class clazz) {
- return getInvocableEngine().getInterface(thiz, clazz);
- }
-
- private Invocable getInvocableEngine() {
- return (Invocable) scriptEngine;
- }
-
- /**
- * Returns {@literal true} iif the {@link ScriptEngine} has a function with the given {@code functionName}.
- * @param functionName
- * @return
- */
- public boolean hasFunction(String functionName){
-
- Object candidate = scriptEngine.getContext().getAttribute(functionName);
-
- return candidate != null;
- }
-}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java b/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
new file mode 100644
index 0000000000..c3859aba35
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/scripting/InvocableScriptAdapter.java
@@ -0,0 +1,118 @@
+/*
+ * 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.scripting;
+
+import org.keycloak.models.ScriptModel;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+
+/**
+ * Wraps a {@link ScriptModel} and makes it {@link Invocable}.
+ *
+ * @author Thomas Darimont
+ */
+public class InvocableScriptAdapter implements Invocable {
+
+ /**
+ * Holds the {@ScriptModel}
+ */
+ private final ScriptModel scriptModel;
+
+ /**
+ * Holds the {@link ScriptEngine} instance initialized with the script code.
+ */
+ private final ScriptEngine scriptEngine;
+
+ /**
+ * Creates a new {@link InvocableScriptAdapter} instance.
+ *
+ * @param scriptModel must not be {@literal null}
+ * @param scriptEngine must not be {@literal null}
+ */
+ public InvocableScriptAdapter(ScriptModel scriptModel, ScriptEngine scriptEngine) {
+
+ if (scriptModel == null) {
+ throw new IllegalArgumentException("scriptModel must not be null");
+ }
+
+ if (scriptEngine == null) {
+ throw new IllegalArgumentException("scriptEngine must not be null");
+ }
+
+ this.scriptModel = scriptModel;
+ this.scriptEngine = loadScriptIntoEngine(scriptModel, scriptEngine);
+ }
+
+ @Override
+ public Object invokeMethod(Object thiz, String name, Object... args) throws ScriptExecutionException {
+
+ try {
+ return getInvocableEngine().invokeMethod(thiz, name, args);
+ } catch (ScriptException | NoSuchMethodException e) {
+ throw new ScriptExecutionException(scriptModel, e);
+ }
+ }
+
+ @Override
+ public Object invokeFunction(String name, Object... args) throws ScriptExecutionException {
+ try {
+ return getInvocableEngine().invokeFunction(name, args);
+ } catch (ScriptException | NoSuchMethodException e) {
+ throw new ScriptExecutionException(scriptModel, e);
+ }
+ }
+
+ @Override
+ public T getInterface(Class clazz) {
+ return getInvocableEngine().getInterface(clazz);
+ }
+
+ @Override
+ public T getInterface(Object thiz, Class clazz) {
+ return getInvocableEngine().getInterface(thiz, clazz);
+ }
+
+ /**
+ * Returns {@literal true} if the {@link ScriptEngine} has a definition with the given {@code name}.
+ *
+ * @param name
+ * @return
+ */
+ public boolean isDefined(String name) {
+
+ Object candidate = scriptEngine.getContext().getAttribute(name);
+
+ return candidate != null;
+ }
+
+ private ScriptEngine loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
+
+ try {
+ engine.eval(script.getCode());
+ } catch (ScriptException se) {
+ throw new ScriptExecutionException(script, se);
+ }
+
+ return engine;
+ }
+
+ private Invocable getInvocableEngine() {
+ return (Invocable) scriptEngine;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/Script.java b/server-spi/src/main/java/org/keycloak/scripting/Script.java
index 2e81372655..ef86902e24 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/Script.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/Script.java
@@ -1,8 +1,26 @@
+/*
+ * 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.scripting;
import org.keycloak.models.ScriptModel;
/**
+ * A {@link ScriptModel} which holds some meta-data.
+ *
* @author Thomas Darimont
*/
public class Script implements ScriptModel {
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
index 9d55195977..9613eb64e4 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptBindingsConfigurer.java
@@ -1,17 +1,33 @@
+/*
+ * 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.scripting;
import javax.script.Bindings;
/**
* Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}.
- *
+ * Used by {@link ScriptingProvider}
* @author Thomas Darimont
*/
@FunctionalInterface
public interface ScriptBindingsConfigurer {
/**
- * A default {@link ScriptBindingsConfigurer} leaves the Bindings empty.
+ * A default {@link ScriptBindingsConfigurer} that provides no Bindings.
*/
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
index e912ca915e..2063bd21b9 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptExecutionException.java
@@ -1,3 +1,19 @@
+/*
+ * 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.scripting;
import org.keycloak.models.ScriptModel;
@@ -11,7 +27,7 @@ import javax.script.ScriptException;
*/
public class ScriptExecutionException extends RuntimeException {
- public ScriptExecutionException(ScriptModel script, ScriptException se) {
- super("Error executing script '" + script.getName() + "'", se);
+ public ScriptExecutionException(ScriptModel script, Exception ex) {
+ super("Could not execute script '" + script.getName() + "' problem was: " + ex.getMessage(), ex);
}
}
diff --git a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
index 163120bc7c..67bad5a9c9 100644
--- a/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
+++ b/server-spi/src/main/java/org/keycloak/scripting/ScriptingProvider.java
@@ -1,3 +1,19 @@
+/*
+ * 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.scripting;
import org.keycloak.models.ScriptModel;
@@ -13,20 +29,23 @@ import javax.script.ScriptEngine;
public interface ScriptingProvider extends Provider {
/**
- * Returns an {@link InvocableScript} based on the given {@link ScriptModel}.
- * The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}
+ * Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
+ * The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}
*
- * @param script the script to wrap
+ * @param scriptModel the scriptModel to wrap
* @param bindingsConfigurer populates the {@link javax.script.Bindings}
* @return
*/
- InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer);
+ InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer);
/**
- * Returns an {@link InvocableScript} based on the given {@link ScriptModel} with an {@link ScriptBindingsConfigurer#EMPTY} {@code ScriptBindingsConfigurer}.
- * @see #prepareScript(ScriptModel, ScriptBindingsConfigurer)
- * @param script
+ * Creates a new {@link ScriptModel} instance.
+ *
+ * @param realmId
+ * @param scriptName
+ * @param scriptCode
+ * @param scriptDescription
* @return
*/
- InvocableScript prepareScript(ScriptModel script);
+ ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription);
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
index 895f0356f0..85a217ff1e 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java
@@ -1,23 +1,82 @@
+/*
+ * 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.authentication.authenticators.browser;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.ScriptModel;
import org.keycloak.models.UserModel;
-import org.keycloak.scripting.InvocableScript;
-import org.keycloak.scripting.Script;
-import org.keycloak.scripting.ScriptBindingsConfigurer;
+import org.keycloak.scripting.InvocableScriptAdapter;
+import org.keycloak.scripting.ScriptExecutionException;
import org.keycloak.scripting.ScriptingProvider;
-import javax.script.Bindings;
-import javax.script.ScriptException;
import java.util.Map;
/**
* An {@link Authenticator} that can execute a configured script during authentication flow.
- * scripts must provide
+ *
+ * Scripts must at least provide one of the following functions:
+ *
+ * - {@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}
+ * - {@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}
+ *
+ *
+ *
+ * Custom {@link Authenticator Authenticator's} should at least provide the {@code authenticate(..)} function.
+ * The following script {@link javax.script.Bindings} are available for convenient use within script code.
+ *
+ * - {@code script} the {@link ScriptModel} to access script metadata
+ * - {@code realm} the {@link RealmModel}
+ * - {@code user} the current {@link UserModel}
+ * - {@code session} the active {@link KeycloakSession}
+ * - {@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}
+ * - {@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
+ *
+ *
+ *
+ * Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
+ * or {@code action(context)} function.
+ *
+ * An example {@link ScriptBasedAuthenticator} definition could look as follows:
+ *
+ * {@code
+ *
+ * AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+ *
+ * function authenticate(context) {
+ *
+ * LOG.info(script.name + " --> trace auth for: " + user.username);
+ *
+ * if ( user.username === "tester"
+ * && user.getAttribute("someAttribute")
+ * && user.getAttribute("someAttribute").contains("someValue")) {
+ *
+ * context.failure(AuthenticationFlowError.INVALID_USER);
+ * return;
+ * }
+ *
+ * context.success();
+ * }
+ * }
+ *
*
* @author Thomas Darimont
*/
@@ -29,58 +88,51 @@ public class ScriptBasedAuthenticator implements Authenticator {
static final String SCRIPT_NAME = "scriptName";
static final String SCRIPT_DESCRIPTION = "scriptDescription";
- static final String ACTION = "action";
- static final String AUTHENTICATE = "authenticate";
- static final String TEXT_JAVASCRIPT = "text/javascript";
+ static final String ACTION_FUNCTION_NAME = "action";
+ static final String AUTHENTICATE_FUNCTION_NAME = "authenticate";
@Override
public void authenticate(AuthenticationFlowContext context) {
- tryInvoke(AUTHENTICATE, context);
+ tryInvoke(AUTHENTICATE_FUNCTION_NAME, context);
}
@Override
public void action(AuthenticationFlowContext context) {
- tryInvoke(ACTION, context);
+ tryInvoke(ACTION_FUNCTION_NAME, context);
}
private void tryInvoke(String functionName, AuthenticationFlowContext context) {
- InvocableScript script = getInvocableScript(context);
+ if (!hasAuthenticatorConfig(context)) {
+ // this is an empty not yet configured script authenticator
+ // we mark this execution as success to not lock out users due to incompletely configured authenticators.
+ context.success();
+ return;
+ }
- if (!script.hasFunction(functionName)) {
+ InvocableScriptAdapter invocableScriptAdapter = getInvocableScriptAdapter(context);
+
+ if (!invocableScriptAdapter.isDefined(functionName)) {
return;
}
try {
- //should context be wrapped in a readonly wrapper?
- script.invokeFunction(functionName, context);
- } catch (ScriptException | NoSuchMethodException e) {
+ //should context be wrapped in a read-only wrapper?
+ invocableScriptAdapter.invokeFunction(functionName, context);
+ } catch (ScriptExecutionException e) {
LOGGER.error(e);
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR);
}
}
- private InvocableScript getInvocableScript(final AuthenticationFlowContext context) {
-
- final Script script = createAdhocScriptFromContext(context);
-
- ScriptBindingsConfigurer bindingsConfigurer = new ScriptBindingsConfigurer() {
-
- @Override
- public void configureBindings(Bindings bindings) {
-
- bindings.put("script", script);
- bindings.put("LOG", LOGGER);
- }
- };
-
- ScriptingProvider scripting = context.getSession().scripting();
-
- //how to deal with long running scripts -> timeout?
-
- return scripting.prepareScript(script, bindingsConfigurer);
+ private boolean hasAuthenticatorConfig(AuthenticationFlowContext context) {
+ return context != null
+ && context.getAuthenticatorConfig() != null
+ && context.getAuthenticatorConfig().getConfig() != null
+ && !context.getAuthenticatorConfig().getConfig().isEmpty();
}
- private Script createAdhocScriptFromContext(AuthenticationFlowContext context) {
+ private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) {
Map config = context.getAuthenticatorConfig().getConfig();
@@ -90,21 +142,35 @@ public class ScriptBasedAuthenticator implements Authenticator {
RealmModel realm = context.getRealm();
- return new Script(null /* scriptId */, realm.getId(), scriptName, TEXT_JAVASCRIPT, scriptCode, scriptDescription);
+ ScriptingProvider scripting = context.getSession().scripting();
+
+ //TODO lookup script by scriptId instead of creating it every time
+ ScriptModel script = scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, scriptName, scriptCode, scriptDescription);
+
+ //how to deal with long running scripts -> timeout?
+ return scripting.prepareInvocableScript(script, bindings -> {
+ bindings.put("script", script);
+ bindings.put("realm", context.getRealm());
+ bindings.put("user", context.getUser());
+ bindings.put("session", context.getSession());
+ bindings.put("httpRequest", context.getHttpRequest());
+ bindings.put("LOG", LOGGER);
+ });
}
@Override
public boolean requiresUser() {
- return false;
+ return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return false;
+ return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ //TODO make RequiredActions configurable in the script
//NOOP
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
index b25af8f1fd..0528154522 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java
@@ -1,5 +1,23 @@
+/*
+ * 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.authentication.authenticators.browser;
+import org.apache.commons.io.IOUtils;
+import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
@@ -8,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
+import java.io.IOException;
import java.util.List;
import static java.util.Arrays.asList;
@@ -24,7 +43,9 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
*/
public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
- static final String PROVIDER_ID = "auth-script-based";
+ private static final Logger LOGGER = Logger.getLogger(ScriptBasedAuthenticatorFactory.class);
+
+ public static final String PROVIDER_ID = "auth-script-based";
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
@@ -77,7 +98,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
@Override
public boolean isUserSetupAllowed() {
- return false;
+ return true;
}
@Override
@@ -87,12 +108,12 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
@Override
public String getDisplayType() {
- return "Script-based Authentication";
+ return "Script";
}
@Override
public String getHelpText() {
- return "Script based authentication.";
+ return "Script based authentication. Allows to define custom authentication logic via JavaScript.";
}
@Override
@@ -114,9 +135,16 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
script.setType(SCRIPT_TYPE);
script.setName(SCRIPT_CODE);
script.setLabel("Script Source");
- script.setDefaultValue("//enter your script here");
- script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate' that accepts a context (AuthenticationFlowContext) parameter." +
- "This authenticator exposes the following additional variables: 'script', 'LOG'");
+
+ String scriptTemplate = "//enter your script code here";
+ try {
+ scriptTemplate = IOUtils.toString(getClass().getResource("/scripts/authenticator-template.js"));
+ } catch (IOException ioe) {
+ LOGGER.warn(ioe);
+ }
+ script.setDefaultValue(scriptTemplate);
+ script.setHelpText("The script used to authenticate. Scripts must at least define a function with the name 'authenticate(context)' that accepts a context (AuthenticationFlowContext) parameter.\n" +
+ "This authenticator exposes the following additional variables: 'script', 'realm', 'user', 'session', 'httpRequest', 'LOG'");
return asList(name, description, script);
}
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
index 140ec04fbd..5772f53055 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProvider.java
@@ -1,3 +1,19 @@
+/*
+ * 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.scripting;
import org.keycloak.models.ScriptModel;
@@ -6,7 +22,6 @@ import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
-import javax.script.ScriptException;
/**
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@@ -18,40 +33,70 @@ public class DefaultScriptingProvider implements ScriptingProvider {
private final ScriptEngineManager scriptEngineManager;
public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
+
+ if (scriptEngineManager == null) {
+ throw new IllegalStateException("scriptEngineManager must not be null!");
+ }
+
this.scriptEngineManager = scriptEngineManager;
}
+ /**
+ * Wraps the provided {@link ScriptModel} in a {@link javax.script.Invocable} instance with bindings configured through the {@link ScriptBindingsConfigurer}.
+ *
+ * @param scriptModel must not be {@literal null}
+ * @param bindingsConfigurer must not be {@literal null}
+ * @return
+ */
@Override
- public InvocableScript prepareScript(ScriptModel script) {
- return prepareScript(script, ScriptBindingsConfigurer.EMPTY);
- }
+ public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
- @Override
- public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
-
- if (script == null) {
- throw new NullPointerException("script must not be null");
+ if (scriptModel == null) {
+ throw new IllegalArgumentException("script must not be null");
}
- if (script.getCode() == null || script.getCode().trim().isEmpty()) {
+ if (scriptModel.getCode() == null || scriptModel.getCode().trim().isEmpty()) {
throw new IllegalArgumentException("script must not be null or empty");
}
if (bindingsConfigurer == null) {
- throw new NullPointerException("bindingsConfigurer must not be null");
+ throw new IllegalArgumentException("bindingsConfigurer must not be null");
}
- ScriptEngine engine = lookupScriptEngineFor(script);
+ ScriptEngine engine = createPreparedScriptEngine(scriptModel, bindingsConfigurer);
- if (engine == null) {
+ return new InvocableScriptAdapter(scriptModel, engine);
+ }
+
+ //TODO allow scripts to be maintained independently of other components, e.g. with dedicated persistence
+ //TODO allow script lookup by (scriptId)
+ //TODO allow script lookup by (name, realmName)
+
+ @Override
+ public ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription) {
+
+ ScriptModel script = new Script(null /* scriptId */, realmId, scriptName, mimeType, scriptCode, scriptDescription);
+ return script;
+ }
+
+ /**
+ * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
+ *
+ * @param script
+ * @param bindingsConfigurer
+ * @return
+ */
+ private ScriptEngine createPreparedScriptEngine(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
+
+ ScriptEngine scriptEngine = lookupScriptEngineFor(script);
+
+ if (scriptEngine == null) {
throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
}
- configureBindings(bindingsConfigurer, engine);
+ configureBindings(bindingsConfigurer, scriptEngine);
- loadScriptIntoEngine(script, engine);
-
- return new InvocableScript(script, engine);
+ return scriptEngine;
}
private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
@@ -61,15 +106,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
}
- private void loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
-
- try {
- engine.eval(script.getCode());
- } catch (ScriptException se) {
- throw new ScriptExecutionException(script, se);
- }
- }
-
+ /**
+ * Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
+ */
private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
return scriptEngineManager.getEngineByMimeType(script.getMimeType());
}
diff --git a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
index 2e8a431aa0..b00a058eee 100644
--- a/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
+++ b/services/src/main/java/org/keycloak/scripting/DefaultScriptingProviderFactory.java
@@ -1,3 +1,19 @@
+/*
+ * 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.scripting;
import org.keycloak.Config;
@@ -13,11 +29,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
static final String ID = "script-based-auth";
- private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
-
@Override
public ScriptingProvider create(KeycloakSession session) {
- return new DefaultScriptingProvider(scriptEngineManager);
+ return new DefaultScriptingProvider(ScriptEngineManagerHolder.SCRIPT_ENGINE_MANAGER);
}
@Override
@@ -39,4 +53,12 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
public String getId() {
return ID;
}
+
+ /**
+ * Holder class for lazy initialization of {@link ScriptEngineManager}.
+ */
+ private static class ScriptEngineManagerHolder {
+
+ private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager();
+ }
}
diff --git a/services/src/main/resources/scripts/authenticator-template.js b/services/src/main/resources/scripts/authenticator-template.js
new file mode 100644
index 0000000000..73bb12475f
--- /dev/null
+++ b/services/src/main/resources/scripts/authenticator-template.js
@@ -0,0 +1,37 @@
+/*
+ * Template for JavaScript based authenticator's.
+ * See org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory
+ */
+
+// import enum for error lookup
+AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+
+/**
+ * An example authenticate function.
+ *
+ * The following variables are available for convenience:
+ * user - current user {@see org.keycloak.models.UserModel}
+ * realm - current realm {@see org.keycloak.models.RealmModel}
+ * session - current KeycloakSession {@see org.keycloak.models.KeycloakSession}
+ * httpRequest - current HttpRequest {@see org.jboss.resteasy.spi.HttpRequest}
+ * script - current script {@see org.keycloak.models.ScriptModel}
+ * LOG - current logger {@see org.jboss.logging.Logger}
+ *
+ * You one can extract current http request headers via:
+ * httpRequest.getHttpHeaders().getHeaderString("Forwarded")
+ *
+ * @param context {@see org.keycloak.authentication.AuthenticationFlowContext}
+ */
+function authenticate(context) {
+
+ LOG.info(script.name + " trace auth for: " + user.username);
+
+ var authShouldFail = false;
+ if (authShouldFail) {
+
+ context.failure(AuthenticationFlowError.INVALID_USER);
+ return;
+ }
+
+ context.success();
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 4e07e6fbcc..0f2133b86c 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -136,7 +136,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.");
addProviderInfo(result, "auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server.");
addProviderInfo(result, "auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form.");
- addProviderInfo(result, "auth-script-based", "Script-based Authentication", "Script based authentication.");
+ addProviderInfo(result, "auth-script-based", "Script", "Script based authentication. Allows to define custom authentication logic via JavaScript.");
addProviderInfo(result, "auth-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.");
addProviderInfo(result, "auth-username-password-form", "Username Password Form",
"Validates a username and password from login form.");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java
new file mode 100644
index 0000000000..667c85ff59
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.forms;
+
+import org.apache.commons.io.IOUtils;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.authentication.AuthenticationFlow;
+import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
+import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.storage.user.UserCredentialAuthenticationProvider;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.util.ExecutionBuilder;
+import org.keycloak.testsuite.util.FlowBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for {@link org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticator}
+ *
+ * @author Thomas Darimont
+ */
+public class ScriptAuthenticatorTest extends AbstractFlowTest {
+
+ UserRepresentation failUser;
+
+ UserRepresentation okayUser;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ private AuthenticationFlowRepresentation flow;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+
+ failUser = UserBuilder.create()
+ .id("fail")
+ .username("fail")
+ .email("fail@test.com")
+ .enabled(true)
+ .password("password")
+ .build();
+
+ okayUser = UserBuilder.create()
+ .id("user")
+ .username("user")
+ .email("user@test.com")
+ .enabled(true)
+ .password("password")
+ .build();
+
+ RealmBuilder.edit(testRealm)
+ .user(failUser)
+ .user(okayUser);
+ }
+
+ @Before
+ public void configureFlows() throws Exception {
+
+ String scriptFlow = "scriptBrowser";
+
+ AuthenticationFlowRepresentation scriptBrowserFlow = FlowBuilder.create()
+ .alias(scriptFlow)
+ .description("dummy pass through registration")
+ .providerId("basic-flow")
+ .topLevel(true)
+ .builtIn(false)
+ .build();
+
+ String scriptAuth = "scriptAuth";
+
+ Response createFlowResponse = testRealm().flows().createFlow(scriptBrowserFlow);
+ Assert.assertEquals(201, createFlowResponse.getStatus());
+
+ RealmRepresentation realm = testRealm().toRepresentation();
+ realm.setBrowserFlow(scriptFlow);
+ realm.setDirectGrantFlow(scriptFlow);
+ testRealm().update(realm);
+
+ this.flow = findFlowByAlias(scriptFlow);
+
+ AuthenticationExecutionRepresentation usernamePasswordFormExecution = ExecutionBuilder.create()
+ .id("username password form")
+ .parentFlow(this.flow.getId())
+ .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
+ .authenticator(UsernamePasswordFormFactory.PROVIDER_ID)
+ .build();
+
+ AuthenticationExecutionRepresentation authScriptExecution = ExecutionBuilder.create()
+ .id(scriptAuth)
+ .parentFlow(this.flow.getId())
+ .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name())
+ .authenticator(ScriptBasedAuthenticatorFactory.PROVIDER_ID)
+ .build();
+
+ Response addExecutionResponse = testRealm().flows().addExecution(usernamePasswordFormExecution);
+ Assert.assertEquals(201, addExecutionResponse.getStatus());
+
+ addExecutionResponse = testRealm().flows().addExecution(authScriptExecution);
+ Assert.assertEquals(201, addExecutionResponse.getStatus());
+
+ Response newExecutionConfigResponse = testRealm().flows().newExecutionConfig(scriptAuth, createScriptAuthConfig(scriptAuth, "authenticator-example.js", "/scripts/authenticator-example.js", "simple script based authenticator"));
+ Assert.assertEquals(201, newExecutionConfigResponse.getStatus());
+ }
+
+ /**
+ * KEYCLOAK-3491
+ */
+ @Test
+ public void loginShouldWorkWithScriptAuthenticator() {
+
+ loginPage.open();
+
+ loginPage.login(okayUser.getUsername(), "password");
+
+ events.expectLogin().user(okayUser.getId()).detail(Details.USERNAME, okayUser.getUsername()).assertEvent();
+ }
+
+ /**
+ * KEYCLOAK-3491
+ */
+ @Test
+ public void loginShouldFailWithScriptAuthenticator() {
+
+ loginPage.open();
+
+ loginPage.login(failUser.getUsername(), "password");
+
+ events.expect(EventType.LOGIN_ERROR).user((String)null).error(Errors.USER_NOT_FOUND).assertEvent();
+ }
+
+ private AuthenticatorConfigRepresentation createScriptAuthConfig(String alias, String scriptName, String scriptCodePath, String scriptDescription) throws IOException {
+
+ AuthenticatorConfigRepresentation configRep = new AuthenticatorConfigRepresentation();
+
+ configRep.setAlias(alias);
+ configRep.getConfig().put("scriptCode", IOUtils.toString(getClass().getResourceAsStream(scriptCodePath)));
+ configRep.getConfig().put("scriptName", scriptName);
+ configRep.getConfig().put("scriptDescription", scriptDescription);
+
+ return configRep;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js
new file mode 100644
index 0000000000..0fc10a426a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/authenticator-example.js
@@ -0,0 +1,10 @@
+AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError");
+
+function authenticate(context) {
+ LOG.info(script.name + " --> trace auth for: " + user.username);
+ if (user.username === "fail") {
+ context.failure(AuthenticationFlowError.INVALID_USER);
+ return;
+ }
+ context.success();
+}
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index f42ce55b22..8d631109d5 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -2118,9 +2118,20 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
$scope.realm = realm;
$scope.flow = flow;
$scope.create = true;
- $scope.config = { config: {}};
$scope.configType = configType;
+ var defaultConfig = {};
+ if (configType && Array.isArray(configType.properties)) {
+ for(var i = 0; i < configType.properties.length; i++) {
+ var property = configType.properties[i];
+ if (property && property.name) {
+ defaultConfig[property.name] = property.defaultValue;
+ }
+ }
+ }
+
+ $scope.config = { config: defaultConfig};
+
$scope.$watch(function() {
return $location.path();
}, function() {
@@ -2145,8 +2156,6 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
//$location.url("/realms");
window.history.back();
};
-
-
});
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
index 97d88762ee..57bbc06de4 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-component-config.html
@@ -33,7 +33,7 @@
-
+
{{config[option.name]}}
diff --git a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
index c3a9a64eab..9253c8c839 100755
--- a/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
+++ b/themes/src/main/resources/theme/keycloak/admin/resources/css/styles.css
@@ -377,6 +377,6 @@ h1 i {
}
.ace_editor {
- height: 400px;
+ height: 600px;
width: 100%;
}
\ No newline at end of file
From 98d2fe15e821df8d39a05fffce9a6b81f8ef8553 Mon Sep 17 00:00:00 2001
From: Bruno Oliveira
Date: Thu, 18 Aug 2016 08:02:05 -0300
Subject: [PATCH 2/9] [KEYCLOAK-2438] - Add display name to social login
buttons [KEYCLOAK-3291] - Names of social identity providers are wrongly
capitalized (eg GitHub vs Github)
---
.../idm/IdentityProviderRepresentation.java | 9 ++++++
.../org/keycloak/models/jpa/RealmAdapter.java | 30 +++++++++++++++----
.../jpa/entities/IdentityProviderEntity.java | 11 +++++++
.../META-INF/jpa-changelog-2.2.0.xml | 6 +++-
.../mongo/keycloak/adapters/RealmAdapter.java | 4 ++-
.../models/IdentityProviderModel.java | 11 +++++++
.../entities/IdentityProviderEntity.java | 9 ++++++
.../models/utils/KeycloakModelUtils.java | 29 +++++++++++++++---
.../models/utils/ModelToRepresentation.java | 1 +
.../models/utils/RepresentationToModel.java | 1 +
.../model/AccountFederatedIdentityBean.java | 16 ++++++++--
.../FreeMarkerLoginFormsProvider.java | 5 ++--
.../model/IdentityProviderBean.java | 26 +++++++++++-----
.../model/IdentityProviderBeanTest.java | 16 +++++-----
.../testsuite/account/AccountTest.java | 24 +++++++++++++++
.../testsuite/admin/AbstractAdminTest.java | 9 +-----
.../testsuite/admin/ConsentsTest.java | 1 +
.../testsuite/admin/IdentityProviderTest.java | 1 +
.../testsuite/admin/PermissionsTest.java | 3 +-
.../testsuite/broker/AbstractBrokerTest.java | 1 +
.../testsuite/i18n/LoginPageTest.java | 29 ++++++++++++++++++
.../util/IdentityProviderBuilder.java | 5 ++++
.../broker-test/test-realm-with-broker.json | 3 ++
.../theme/base/account/federatedIdentity.ftl | 2 +-
.../messages/admin-messages_en.properties | 2 ++
.../admin/resources/js/controllers/realm.js | 16 +++++++++-
.../identity-provider-mapper-detail.html | 2 +-
.../partials/identity-provider-mappers.html | 2 +-
.../realm-identity-provider-export.html | 2 +-
.../realm-identity-provider-oidc.html | 9 +++++-
.../realm-identity-provider-saml.html | 9 +++++-
.../realm-identity-provider-social.html | 2 +-
.../partials/realm-identity-provider.html | 2 +-
.../templates/kc-tabs-identity-provider.html | 2 +-
.../main/resources/theme/base/login/login.ftl | 2 +-
.../keycloak/login/resources/css/login.css | 3 --
36 files changed, 249 insertions(+), 56 deletions(-)
mode change 100755 => 100644 themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
index 2466f12e4f..40b530181e 100755
--- a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java
@@ -25,6 +25,7 @@ import java.util.Map;
public class IdentityProviderRepresentation {
protected String alias;
+ protected String displayName;
protected String internalId;
protected String providerId;
protected boolean enabled = true;
@@ -176,4 +177,12 @@ public class IdentityProviderRepresentation {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index a5747c6285..21aa8d0648 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -18,10 +18,9 @@
package org.keycloak.models.jpa;
import org.jboss.logging.Logger;
-import org.keycloak.common.util.MultivaluedHashMap;
-import org.keycloak.common.util.StringPropertyReplacer;
-import org.keycloak.component.ComponentModel;
import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.MultivaluedHashMap;
+import org.keycloak.component.ComponentModel;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
@@ -43,12 +42,28 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderCreationEventImpl;
import org.keycloak.models.UserFederationProviderModel;
-import org.keycloak.models.jpa.entities.*;
+import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity;
+import org.keycloak.models.jpa.entities.AuthenticationFlowEntity;
+import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity;
+import org.keycloak.models.jpa.entities.ClientEntity;
+import org.keycloak.models.jpa.entities.ClientTemplateEntity;
+import org.keycloak.models.jpa.entities.ComponentConfigEntity;
+import org.keycloak.models.jpa.entities.ComponentEntity;
+import org.keycloak.models.jpa.entities.GroupEntity;
+import org.keycloak.models.jpa.entities.IdentityProviderEntity;
+import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity;
+import org.keycloak.models.jpa.entities.RealmAttributeEntity;
+import org.keycloak.models.jpa.entities.RealmAttributes;
+import org.keycloak.models.jpa.entities.RealmEntity;
+import org.keycloak.models.jpa.entities.RequiredActionProviderEntity;
+import org.keycloak.models.jpa.entities.RequiredCredentialEntity;
+import org.keycloak.models.jpa.entities.RoleEntity;
+import org.keycloak.models.jpa.entities.UserFederationMapperEntity;
+import org.keycloak.models.jpa.entities.UserFederationProviderEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
-
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
@@ -1255,9 +1270,10 @@ public class RealmAdapter implements RealmModel, JpaModel {
for (IdentityProviderEntity entity: entities) {
IdentityProviderModel identityProviderModel = new IdentityProviderModel();
-
identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias());
+ identityProviderModel.setDisplayName(entity.getDisplayName());
+
identityProviderModel.setInternalId(entity.getInternalId());
Map config = entity.getConfig();
Map copy = new HashMap<>();
@@ -1294,6 +1310,7 @@ public class RealmAdapter implements RealmModel, JpaModel {
entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled());
entity.setStoreToken(identityProvider.isStoreToken());
@@ -1327,6 +1344,7 @@ public class RealmAdapter implements RealmModel, JpaModel {
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
index 77ca6147ed..9a94431208 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/IdentityProviderEntity.java
@@ -59,6 +59,9 @@ public class IdentityProviderEntity {
@Column(name="PROVIDER_ALIAS")
private String alias;
+ @Column(name="PROVIDER_DISPLAY_NAME")
+ private String displayName;
+
@Column(name="ENABLED")
private boolean enabled;
@@ -182,6 +185,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.2.0.xml
index adc49fbf3d..39e7fe4a12 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.2.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.2.0.xml
@@ -24,6 +24,10 @@
+
+
+
+
@@ -58,4 +62,4 @@
-
\ No newline at end of file
+
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index 86a90013cc..85592b0483 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -21,7 +21,6 @@ import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.common.util.MultivaluedHashMap;
-import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.component.ComponentModel;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.common.enums.SslRequired;
@@ -914,6 +913,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
identityProviderModel.setProviderId(entity.getProviderId());
identityProviderModel.setAlias(entity.getAlias());
+ identityProviderModel.setDisplayName(entity.getDisplayName());
identityProviderModel.setInternalId(entity.getInternalId());
Map config = entity.getConfig();
Map copy = new HashMap<>();
@@ -950,6 +950,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
entity.setInternalId(KeycloakModelUtils.generateId());
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setProviderId(identityProvider.getProviderId());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
@@ -980,6 +981,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme
for (IdentityProviderEntity entity : this.realm.getIdentityProviders()) {
if (entity.getInternalId().equals(identityProvider.getInternalId())) {
entity.setAlias(identityProvider.getAlias());
+ entity.setDisplayName(identityProvider.getDisplayName());
entity.setEnabled(identityProvider.isEnabled());
entity.setTrustEmail(identityProvider.isTrustEmail());
entity.setAuthenticateByDefault(identityProvider.isAuthenticateByDefault());
diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
index 2425c7d877..41a1e41fee 100755
--- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java
@@ -57,6 +57,8 @@ public class IdentityProviderModel implements Serializable {
private String postBrokerLoginFlowId;
+ private String displayName;
+
/**
* A map containing the configuration and properties for a specific identity provider instance and implementation. The items
* in the map are understood by the identity provider implementation.
@@ -70,6 +72,7 @@ public class IdentityProviderModel implements Serializable {
this.internalId = model.getInternalId();
this.providerId = model.getProviderId();
this.alias = model.getAlias();
+ this.displayName = model.getDisplayName();
this.config = new HashMap(model.getConfig());
this.enabled = model.isEnabled();
this.trustEmail = model.isTrustEmail();
@@ -169,5 +172,13 @@ public class IdentityProviderModel implements Serializable {
public void setTrustEmail(boolean trustEmail) {
this.trustEmail = trustEmail;
}
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java b/server-spi/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java
index e23198e696..b0c39ffa75 100755
--- a/server-spi/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java
+++ b/server-spi/src/main/java/org/keycloak/models/entities/IdentityProviderEntity.java
@@ -26,6 +26,7 @@ public class IdentityProviderEntity {
private String internalId;
private String alias;
+ private String displayName;
private String providerId;
private String name;
private boolean enabled;
@@ -134,6 +135,14 @@ public class IdentityProviderEntity {
this.trustEmail = trustEmail;
}
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index b8adc6271d..928963cf17 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -18,6 +18,8 @@
package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.common.util.Base64Url;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
@@ -47,8 +49,6 @@ import org.keycloak.common.util.PemUtils;
import org.keycloak.transaction.JtaTransactionManagerLookup;
import javax.crypto.spec.SecretKeySpec;
-import javax.naming.InitialContext;
-import javax.sql.DataSource;
import javax.transaction.InvalidTransactionException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
@@ -62,7 +62,6 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
-import java.sql.DriverManager;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -70,7 +69,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.function.Function;
/**
* Set of helper methods, which are useful in various model implementations.
@@ -689,4 +687,27 @@ public final class KeycloakModelUtils {
}
}
+
+ /**
+ * Retrieve display name based on identity provider type
+ * @param session
+ * @param displayName
+ * @param providerId
+ * @return
+ */
+ public static String getIdentityProviderDisplayName(KeycloakSession session, String displayName, String providerId) {
+
+ SocialIdentityProviderFactory providerFactory = (SocialIdentityProviderFactory) session.getKeycloakSessionFactory()
+ .getProviderFactory(SocialIdentityProvider.class, providerId);
+
+ if ((displayName == null || displayName.isEmpty()) && (providerId.contains("saml") || providerId.contains("oidc"))) {
+ return providerId;
+ } else if (providerFactory != null && (displayName == null || displayName.isEmpty())) {
+ return providerFactory.getName();
+ } else {
+ return displayName;
+ }
+ }
+
+
}
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 659c116419..e5e888d27f 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -627,6 +627,7 @@ public class ModelToRepresentation {
providerRep.setInternalId(identityProviderModel.getInternalId());
providerRep.setProviderId(identityProviderModel.getProviderId());
providerRep.setAlias(identityProviderModel.getAlias());
+ providerRep.setDisplayName(identityProviderModel.getDisplayName());
providerRep.setEnabled(identityProviderModel.isEnabled());
providerRep.setStoreToken(identityProviderModel.isStoreToken());
providerRep.setTrustEmail(identityProviderModel.isTrustEmail());
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index f228bc7631..4cfdfd06bc 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1524,6 +1524,7 @@ public class RepresentationToModel {
identityProviderModel.setInternalId(representation.getInternalId());
identityProviderModel.setAlias(representation.getAlias());
+ identityProviderModel.setDisplayName(representation.getDisplayName());
identityProviderModel.setProviderId(representation.getProviderId());
identityProviderModel.setEnabled(representation.isEnabled());
identityProviderModel.setTrustEmail(representation.isTrustEmail());
diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
index a20810e8a0..5ef319c206 100755
--- a/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
+++ b/services/src/main/java/org/keycloak/forms/account/freemarker/model/AccountFederatedIdentityBean.java
@@ -22,6 +22,7 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.Urls;
@@ -70,7 +71,9 @@ public class AccountFederatedIdentityBean {
.queryParam("stateChecker", stateChecker)
.build().toString();
- FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, provider.getAlias(), provider.getAlias(), actionUrl,
+ String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session,
+ provider.getDisplayName(), provider.getProviderId());
+ FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, displayName, provider.getAlias(), provider.getAlias(), actionUrl,
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
orderedSet.add(entry);
}
@@ -106,10 +109,12 @@ public class AccountFederatedIdentityBean {
private final String providerName;
private final String actionUrl;
private final String guiOrder;
+ private final String displayName;
- public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String providerId, String providerName, String actionUrl, String guiOrder
- ) {
+ public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String displayName, String providerId,
+ String providerName, String actionUrl, String guiOrder) {
this.federatedIdentityModel = federatedIdentityModel;
+ this.displayName = displayName;
this.providerId = providerId;
this.providerName = providerName;
this.actionUrl = actionUrl;
@@ -143,6 +148,11 @@ public class AccountFederatedIdentityBean {
public String getGuiOrder() {
return guiOrder;
}
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
}
public static class IdentityProviderComparator implements Comparator {
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index 631ff5792e..515ab76d48 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -250,7 +250,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
- attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
+ attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
@@ -398,7 +398,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
- attributes.put("social", new IdentityProviderBean(realm, identityProviders, baseUri, uriInfo));
+ attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
@@ -425,7 +425,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
}
-
@Override
public Response createLogin() {
return createResponse(LoginFormsPages.LOGIN);
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
index 830166039c..c31bea748f 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java
@@ -17,7 +17,9 @@
package org.keycloak.forms.login.freemarker.model;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.Urls;
import javax.ws.rs.core.UriInfo;
@@ -35,12 +37,13 @@ import java.util.TreeSet;
public class IdentityProviderBean {
private boolean displaySocial;
-
private List providers;
private RealmModel realm;
+ private final KeycloakSession session;
- public IdentityProviderBean(RealmModel realm, List identityProviders, URI baseURI, UriInfo uriInfo) {
+ public IdentityProviderBean(RealmModel realm, KeycloakSession session, List identityProviders, URI baseURI, UriInfo uriInfo) {
this.realm = realm;
+ this.session = session;
if (!identityProviders.isEmpty()) {
Set orderedSet = new TreeSet<>(IdentityProviderComparator.INSTANCE);
@@ -59,7 +62,11 @@ public class IdentityProviderBean {
private void addIdentityProvider(Set orderedSet, RealmModel realm, URI baseURI, IdentityProviderModel identityProvider) {
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getAlias(), realm.getName()).toString();
- orderedSet.add(new IdentityProvider(identityProvider.getAlias(), identityProvider.getProviderId(), loginUrl,
+ String displayName = KeycloakModelUtils.getIdentityProviderDisplayName(session,
+ identityProvider.getDisplayName(), identityProvider.getProviderId());
+
+ orderedSet.add(new IdentityProvider(identityProvider.getAlias(),
+ displayName, identityProvider.getProviderId(), loginUrl,
identityProvider.getConfig() != null ? identityProvider.getConfig().get("guiOrder") : null));
}
@@ -77,9 +84,11 @@ public class IdentityProviderBean {
private final String providerId; // This refer to providerType (facebook, google, etc.)
private final String loginUrl;
private final String guiOrder;
+ private final String displayName;
- public IdentityProvider(String alias, String providerId, String loginUrl, String guiOrder) {
+ public IdentityProvider(String alias, String displayName, String providerId, String loginUrl, String guiOrder) {
this.alias = alias;
+ this.displayName = displayName;
this.providerId = providerId;
this.loginUrl = loginUrl;
this.guiOrder = guiOrder;
@@ -100,6 +109,10 @@ public class IdentityProviderBean {
public String getGuiOrder() {
return guiOrder;
}
+
+ public String getDisplayName() {
+ return displayName;
+ }
}
public static class IdentityProviderComparator implements Comparator {
@@ -112,7 +125,7 @@ public class IdentityProviderBean {
@Override
public int compare(IdentityProvider o1, IdentityProvider o2) {
-
+
int o1order = parseOrder(o1);
int o2order = parseOrder(o2);
@@ -120,7 +133,7 @@ public class IdentityProviderBean {
return 1;
else if (o1order < o2order)
return -1;
-
+
return 1;
}
@@ -134,6 +147,5 @@ public class IdentityProviderBean {
}
return 10000;
}
-
}
}
diff --git a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
index ff4c3686a8..0598ef635c 100755
--- a/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
+++ b/services/src/test/java/org/keycloak/test/login/freemarker/model/IdentityProviderBeanTest.java
@@ -32,32 +32,32 @@ public class IdentityProviderBeanTest {
@Test
public void testIdentityProviderComparator() {
- IdentityProvider o1 = new IdentityProvider("alias1", "id1", "ur1", null);
- IdentityProvider o2 = new IdentityProvider("alias2", "id2", "ur2", null);
+ IdentityProvider o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", null);
+ IdentityProvider o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", null);
// guiOrder not defined at any object - first is always lower
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is not a number so it is same as not defined - first is always lower
- o1 = new IdentityProvider("alias1", "id1", "ur1", "not a number");
+ o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "not a number");
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is defined for one only to it is always first
- o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
+ o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is defined for both but is same - first is always lower
- o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
- o2 = new IdentityProvider("alias2", "id2", "ur2", "0");
+ o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
+ o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "0");
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
// guiOrder is reflected
- o1 = new IdentityProvider("alias1", "id1", "ur1", "0");
- o2 = new IdentityProvider("alias2", "id2", "ur2", "1");
+ o1 = new IdentityProvider("alias1", "displayName1", "id1", "ur1", "0");
+ o2 = new IdentityProvider("alias2", "displayName2", "id2", "ur2", "1");
Assert.assertEquals(-1, IdentityProviderComparator.INSTANCE.compare(o1, o2));
Assert.assertEquals(1, IdentityProviderComparator.INSTANCE.compare(o2, o1));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 35d11d15f4..4d45b2182f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -38,6 +38,7 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.RegisterPage;
+import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@@ -75,6 +76,20 @@ public class AccountTest extends TestRealmKeycloakTest {
.password("password")
.build();
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("github")
+ .alias("github")
+ .build());
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("saml")
+ .alias("saml")
+ .build());
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("oidc")
+ .alias("oidc")
+ .displayName("MyOIDC")
+ .build());
+
RealmBuilder.edit(testRealm)
.user(user2);
}
@@ -790,4 +805,13 @@ public class AccountTest extends TestRealmKeycloakTest {
events.clear();
}
+ @Test
+ public void testIdentityProviderCapitalization(){
+ loginPage.open();
+ Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
+ Assert.assertEquals("saml", loginPage.findSocialButton("saml").getText());
+ Assert.assertEquals("MyOIDC", loginPage.findSocialButton("oidc").getText());
+
+ }
+
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java
index e538dcc5e5..5d51db4fc3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java
@@ -20,9 +20,6 @@ package org.keycloak.testsuite.admin;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -31,16 +28,12 @@ import org.junit.Before;
import org.junit.Rule;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.TestRealmKeycloakTest;
import org.keycloak.testsuite.events.EventsListenerProviderFactory;
import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.util.JsonSerialization;
-import static org.junit.Assert.assertArrayEquals;
-
/**
* This class adapts the functionality from the old testsuite to make tests
* easier to port.
@@ -109,4 +102,4 @@ public abstract class AbstractAdminTest extends TestRealmKeycloakTest {
}
}
-}
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
index fb0a8635db..8407610d2d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ConsentsTest.java
@@ -205,6 +205,7 @@ public class ConsentsTest extends AbstractKeycloakTest {
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
identityProviderRepresentation.setAlias(alias);
+ identityProviderRepresentation.setDisplayName(providerId);
identityProviderRepresentation.setProviderId(providerId);
identityProviderRepresentation.setEnabled(true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
index b9102e118a..834fc365cb 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java
@@ -177,6 +177,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
IdentityProviderRepresentation idp = new IdentityProviderRepresentation();
idp.setAlias(id);
+ idp.setDisplayName(id);
idp.setProviderId(providerId);
idp.setEnabled(true);
if (config != null) {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
index b6fca90ed3..a0ed2c1fd5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java
@@ -1440,7 +1440,8 @@ public class PermissionsTest extends AbstractKeycloakTest {
}, Resource.IDENTITY_PROVIDER, false);
invoke(new InvocationWithResponse() {
public void invoke(RealmResource realm, AtomicReference response) {
- response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch").alias("foo").build()));
+ response.set(realm.identityProviders().create(IdentityProviderBuilder.create().providerId("nosuch")
+ .displayName("nosuch-foo").alias("foo").build()));
}
}, Resource.IDENTITY_PROVIDER, true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
index 29f796f6f5..cff20055ea 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBrokerTest.java
@@ -126,6 +126,7 @@ public abstract class AbstractBrokerTest extends AbstractKeycloakTest {
IdentityProviderRepresentation identityProviderRepresentation = new IdentityProviderRepresentation();
identityProviderRepresentation.setAlias(alias);
+ identityProviderRepresentation.setDisplayName(providerId);
identityProviderRepresentation.setProviderId(providerId);
identityProviderRepresentation.setEnabled(true);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 2cd04fa5f3..6f6e98946d 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -23,10 +23,12 @@ import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.adapters.HttpClientBuilder;
+import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.pages.LoginPage;
import javax.ws.rs.core.Response;
import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.util.IdentityProviderBuilder;
/**
* @author Michael Gerber
@@ -37,6 +39,24 @@ public class LoginPageTest extends AbstractI18NTest {
@Page
protected LoginPage loginPage;
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("github")
+ .alias("github")
+ .build());
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("saml")
+ .alias("saml")
+ .build());
+ testRealm.addIdentityProvider(IdentityProviderBuilder.create()
+ .providerId("oidc")
+ .alias("oidc")
+ .displayName("MyOIDC")
+ .build());
+
+ }
+
@Test
public void languageDropdown() {
loginPage.open();
@@ -87,4 +107,13 @@ public class LoginPageTest extends AbstractI18NTest {
response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
}
+
+ @Test
+ public void testIdentityProviderCapitalization(){
+ loginPage.open();
+ Assert.assertEquals("GitHub", loginPage.findSocialButton("github").getText());
+ Assert.assertEquals("saml", loginPage.findSocialButton("saml").getText());
+ Assert.assertEquals("MyOIDC", loginPage.findSocialButton("oidc").getText());
+
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
index bc3dae6d04..7cb1446df6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/IdentityProviderBuilder.java
@@ -44,6 +44,11 @@ public class IdentityProviderBuilder {
return this;
}
+ public IdentityProviderBuilder displayName(String displayName) {
+ rep.setDisplayName(displayName);
+ return this;
+ }
+
public IdentityProviderRepresentation build() {
return rep;
}
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 dba9c1505d..ab707149f2 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
@@ -94,6 +94,7 @@
{
"alias" : "model-saml-signed-idp",
"providerId" : "saml",
+ "displayName": "My SAML",
"enabled": true,
"config": {
"singleSignOnServiceUrl": "http://localhost:8082/auth/realms/realm-with-saml-identity-provider/protocol/saml",
@@ -157,6 +158,7 @@
{
"alias" : "model-oidc-idp",
"providerId" : "oidc",
+ "displayName": "My OIDC",
"enabled": false,
"authenticateByDefault" : "false",
"config": {
@@ -172,6 +174,7 @@
{
"alias" : "kc-oidc-idp",
"providerId" : "keycloak-oidc",
+ "displayName": "My Keycloak OIDC",
"enabled": true,
"storeToken" : true,
"addReadTokenRoleOnCreate": true,
diff --git a/themes/src/main/resources/theme/base/account/federatedIdentity.ftl b/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
index 3a18805d78..9a901733ce 100755
--- a/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
+++ b/themes/src/main/resources/theme/base/account/federatedIdentity.ftl
@@ -11,7 +11,7 @@
<#list federatedIdentity.identities as identity>