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: + *

    + *
  1. {@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}
  2. + *
  3. {@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}
  4. + *
+ *

+ *

+ * 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. + *

    + *
  1. {@code script} the {@link ScriptModel} to access script metadata
  2. + *
  3. {@code realm} the {@link RealmModel}
  4. + *
  5. {@code user} the current {@link UserModel}
  6. + *
  7. {@code session} the active {@link KeycloakSession}
  8. + *
  9. {@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}
  10. + *
  11. {@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>
- +
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index d3cdd9ee82..4857924a96 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -418,7 +418,9 @@ post-broker-login-flow=Post Login Flow redirect-uri=Redirect URI redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. alias=Alias +display-name=Display Name identity-provider.alias.tooltip=The alias uniquely identifies an identity provider and it is also used to build the redirect uri. +identity-provider.display-name.tooltip=Friendly name for Identity Providers. identity-provider.enabled.tooltip=Enable/disable this identity provider. authenticate-by-default=Authenticate by Default identity-provider.authenticate-by-default.tooltip=Indicates if this provider should be tried by default for authentication even before displaying login screen. 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 old mode 100755 new mode 100644 index 5db9cf0495..111af84e75 --- 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 @@ -689,6 +689,14 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, clien module.controller('IdentityProviderTabCtrl', function(Dialog, $scope, Current, Notifications, $location) { + for (var i in $scope.allProviders) { + var provider = $scope.allProviders[i]; + if (provider.groupName == 'Social' && (provider.id == $scope.identityProvider.alias)) { + $scope.identityProvider.displayName = provider.name; + } else if (!$scope.identityProvider.displayName) { + $scope.identityProvider.displayName = provider.id; + } + } $scope.removeIdentityProvider = function() { Dialog.confirmDelete($scope.identityProvider.alias, 'provider', function() { $scope.identityProvider.$remove({ @@ -773,6 +781,8 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.identityProvider.config = {}; $scope.identityProvider.alias = providerFactory.id; $scope.identityProvider.providerId = providerFactory.id; + $scope.identityProvider.displayName = providerFactory.displayName; + $scope.identityProvider.enabled = true; $scope.identityProvider.authenticateByDefault = false; $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login'; @@ -836,7 +846,6 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload } } - $scope.uploadFile = function() { if (!$scope.identityProvider.alias) { Notifications.error("You must specify an alias"); @@ -909,10 +918,15 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload if (provider.groupName == 'Social' && (provider.id == configProvidedId)) { $scope.allProviders.splice(i, 1); + configuredProviders[j].displayName = provider.name; + break; + } else if (!configuredProviders[j].displayName) { + configuredProviders[j].displayName = configProvidedId; break; } } } + $scope.configuredProviders = angular.copy(configuredProviders); } }, true); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html index cd2964d632..a7c54f804e 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html @@ -1,7 +1,7 @@