keycloak-aplcache

Changes

Details

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 8d6d5fd..c4b9735 100644
--- a/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ScriptModel.java
@@ -1,13 +1,34 @@
+/*
+ * 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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 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.
      */
     String getId();
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 2e81372..ef86902 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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 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 9d55195..9613eb6 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}.
- *
+ * <p>Used by {@link ScriptingProvider}</p>
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
 @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 e912ca9..2063bd2 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 163120b..67bad5a 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}.
-     * <p>The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
+     * Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
+     * <p>The {@code InvocableScriptAdapter} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
      *
-     * @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 895f035..85a217f 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.
- * <p>scripts must provide </p>
+ * <p>
+ * Scripts must at least provide one of the following functions:
+ * <ol>
+ * <li>{@code authenticate(..)} which is called from {@link Authenticator#authenticate(AuthenticationFlowContext)}</li>
+ * <li>{@code action(..)} which is called from {@link Authenticator#action(AuthenticationFlowContext)}</li>
+ * </ol>
+ * </p>
+ * <p>
+ * 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.
+ * <ol>
+ * <li>{@code script} the {@link ScriptModel} to access script metadata</li>
+ * <li>{@code realm} the {@link RealmModel}</li>
+ * <li>{@code user} the current {@link UserModel}</li>
+ * <li>{@code session} the active {@link KeycloakSession}</li>
+ * <li>{@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}</li>
+ * <li>{@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li>
+ * </ol>
+ * </p>
+ * <p>
+ * Additional context information can be extracted from the {@code context} argument passed to the {@code authenticate(context)}
+ * or {@code action(context)} function.
+ * <p>
+ * An example {@link ScriptBasedAuthenticator} definition could look as follows:
+ * <pre>
+ * {@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();
+ *   }
+ * }
+ * </pre>
  *
  * @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
  */
@@ -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;
+        }
+
+        InvocableScriptAdapter invocableScriptAdapter = getInvocableScriptAdapter(context);
 
-        if (!script.hasFunction(functionName)) {
+        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<String, String> 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 b25af8f..0528154 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 140ec04..5772f53 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) {
-        this.scriptEngineManager = scriptEngineManager;
-    }
 
-    @Override
-    public InvocableScript prepareScript(ScriptModel script) {
-        return prepareScript(script, ScriptBindingsConfigurer.EMPTY);
+        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, ScriptBindingsConfigurer bindingsConfigurer) {
+    public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, 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);
+
+        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) {
 
-        if (engine == null) {
+        ScriptEngine scriptEngine = lookupScriptEngineFor(script);
+
+        if (scriptEngine == null) {
             throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
         }
 
-        configureBindings(bindingsConfigurer, engine);
-
-        loadScriptIntoEngine(script, engine);
+        configureBindings(bindingsConfigurer, scriptEngine);
 
-        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 2e8a431..b00a058 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 0000000..73bb124
--- /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 4e07e6f..0f2133b 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 0000000..667c85f
--- /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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
+ */
+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 0000000..0fc10a4
--- /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 4b5b839..deb90a7 100644
--- 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
@@ -2138,9 +2138,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() {
@@ -2165,8 +2176,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 97d8876..57bbc06 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 @@
         </div>
 
         <div class="col-md-6" data-ng-show="option.type == 'Script'">
-            <div ng-model="config[option.name][0]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
+            <div ng-model="config[option.name]" placeholder="Enter your script..." ui-ace="{ useWrapMode: true, showGutter: true, theme:'github', mode: 'javascript'}">
                 {{config[option.name]}}
             </div>
         </div>
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 c3a9a64..9253c8c 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