Merge pull request #3189 from thomasdarimont/issue/KEYCLOAK-3491-revise-scripting-support
KEYCLOAK-3491 Revise Scripting Support
This commit is contained in:
commit
5d34b7e682
18 changed files with 693 additions and 158 deletions
|
@ -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;
|
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>
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
*/
|
*/
|
||||||
public interface ScriptModel {
|
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.
|
* Returns the unique id of the script. {@literal null} for ad-hoc created scripts.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
|
||||||
*/
|
|
||||||
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> T getInterface(Class<T> clazz) {
|
|
||||||
return getInvocableEngine().getInterface(clazz);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getInterface(Object thiz, Class<T> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
|
*/
|
||||||
|
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> T getInterface(Class<T> clazz) {
|
||||||
|
return getInvocableEngine().getInterface(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getInterface(Object thiz, Class<T> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import org.keycloak.models.ScriptModel;
|
import org.keycloak.models.ScriptModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A {@link ScriptModel} which holds some meta-data.
|
||||||
|
*
|
||||||
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
*/
|
*/
|
||||||
public class Script implements ScriptModel {
|
public class Script implements ScriptModel {
|
||||||
|
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import javax.script.Bindings;
|
import javax.script.Bindings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback interface for customization of {@link Bindings} for a {@link javax.script.ScriptEngine}.
|
* 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>
|
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ScriptBindingsConfigurer {
|
public interface ScriptBindingsConfigurer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default {@link ScriptBindingsConfigurer} leaves the Bindings empty.
|
* A default {@link ScriptBindingsConfigurer} that provides no Bindings.
|
||||||
*/
|
*/
|
||||||
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
|
ScriptBindingsConfigurer EMPTY = new ScriptBindingsConfigurer() {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import org.keycloak.models.ScriptModel;
|
import org.keycloak.models.ScriptModel;
|
||||||
|
@ -11,7 +27,7 @@ import javax.script.ScriptException;
|
||||||
*/
|
*/
|
||||||
public class ScriptExecutionException extends RuntimeException {
|
public class ScriptExecutionException extends RuntimeException {
|
||||||
|
|
||||||
public ScriptExecutionException(ScriptModel script, ScriptException se) {
|
public ScriptExecutionException(ScriptModel script, Exception ex) {
|
||||||
super("Error executing script '" + script.getName() + "'", se);
|
super("Could not execute script '" + script.getName() + "' problem was: " + ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import org.keycloak.models.ScriptModel;
|
import org.keycloak.models.ScriptModel;
|
||||||
|
@ -13,20 +29,23 @@ import javax.script.ScriptEngine;
|
||||||
public interface ScriptingProvider extends Provider {
|
public interface ScriptingProvider extends Provider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an {@link InvocableScript} based on the given {@link ScriptModel}.
|
* Returns an {@link InvocableScriptAdapter} based on the given {@link ScriptModel}.
|
||||||
* <p>The {@code InvocableScript} wraps a dedicated {@link ScriptEngine} that was populated with the provided {@link ScriptBindingsConfigurer}</p>
|
* <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}
|
* @param bindingsConfigurer populates the {@link javax.script.Bindings}
|
||||||
* @return
|
* @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}.
|
* Creates a new {@link ScriptModel} instance.
|
||||||
* @see #prepareScript(ScriptModel, ScriptBindingsConfigurer)
|
*
|
||||||
* @param script
|
* @param realmId
|
||||||
|
* @param scriptName
|
||||||
|
* @param scriptCode
|
||||||
|
* @param scriptDescription
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
InvocableScript prepareScript(ScriptModel script);
|
ScriptModel createScript(String realmId, String mimeType, String scriptName, String scriptCode, String scriptDescription);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.ScriptModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.scripting.InvocableScript;
|
import org.keycloak.scripting.InvocableScriptAdapter;
|
||||||
import org.keycloak.scripting.Script;
|
import org.keycloak.scripting.ScriptExecutionException;
|
||||||
import org.keycloak.scripting.ScriptBindingsConfigurer;
|
|
||||||
import org.keycloak.scripting.ScriptingProvider;
|
import org.keycloak.scripting.ScriptingProvider;
|
||||||
|
|
||||||
import javax.script.Bindings;
|
|
||||||
import javax.script.ScriptException;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An {@link Authenticator} that can execute a configured script during authentication flow.
|
* 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>
|
* @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_NAME = "scriptName";
|
||||||
static final String SCRIPT_DESCRIPTION = "scriptDescription";
|
static final String SCRIPT_DESCRIPTION = "scriptDescription";
|
||||||
|
|
||||||
static final String ACTION = "action";
|
static final String ACTION_FUNCTION_NAME = "action";
|
||||||
static final String AUTHENTICATE = "authenticate";
|
static final String AUTHENTICATE_FUNCTION_NAME = "authenticate";
|
||||||
static final String TEXT_JAVASCRIPT = "text/javascript";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void authenticate(AuthenticationFlowContext context) {
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
tryInvoke(AUTHENTICATE, context);
|
tryInvoke(AUTHENTICATE_FUNCTION_NAME, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void action(AuthenticationFlowContext context) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
tryInvoke(ACTION, context);
|
tryInvoke(ACTION_FUNCTION_NAME, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryInvoke(String functionName, AuthenticationFlowContext 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//should context be wrapped in a readonly wrapper?
|
//should context be wrapped in a read-only wrapper?
|
||||||
script.invokeFunction(functionName, context);
|
invocableScriptAdapter.invokeFunction(functionName, context);
|
||||||
} catch (ScriptException | NoSuchMethodException e) {
|
} catch (ScriptExecutionException e) {
|
||||||
LOGGER.error(e);
|
LOGGER.error(e);
|
||||||
|
context.failure(AuthenticationFlowError.INTERNAL_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InvocableScript getInvocableScript(final AuthenticationFlowContext context) {
|
private boolean hasAuthenticatorConfig(AuthenticationFlowContext context) {
|
||||||
|
return context != null
|
||||||
final Script script = createAdhocScriptFromContext(context);
|
&& context.getAuthenticatorConfig() != null
|
||||||
|
&& context.getAuthenticatorConfig().getConfig() != null
|
||||||
ScriptBindingsConfigurer bindingsConfigurer = new ScriptBindingsConfigurer() {
|
&& !context.getAuthenticatorConfig().getConfig().isEmpty();
|
||||||
|
|
||||||
@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 Script createAdhocScriptFromContext(AuthenticationFlowContext context) {
|
private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) {
|
||||||
|
|
||||||
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
||||||
|
|
||||||
|
@ -90,21 +142,35 @@ public class ScriptBasedAuthenticator implements Authenticator {
|
||||||
|
|
||||||
RealmModel realm = context.getRealm();
|
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
|
@Override
|
||||||
public boolean requiresUser() {
|
public boolean requiresUser() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
|
//TODO make RequiredActions configurable in the script
|
||||||
//NOOP
|
//NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorFactory;
|
import org.keycloak.authentication.AuthenticatorFactory;
|
||||||
|
@ -8,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
|
@ -24,7 +43,9 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
|
||||||
*/
|
*/
|
||||||
public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
|
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 = {
|
static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||||
|
@ -77,7 +98,7 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isUserSetupAllowed() {
|
public boolean isUserSetupAllowed() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -87,12 +108,12 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayType() {
|
public String getDisplayType() {
|
||||||
return "Script-based Authentication";
|
return "Script";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getHelpText() {
|
public String getHelpText() {
|
||||||
return "Script based authentication.";
|
return "Script based authentication. Allows to define custom authentication logic via JavaScript.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -114,9 +135,16 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory {
|
||||||
script.setType(SCRIPT_TYPE);
|
script.setType(SCRIPT_TYPE);
|
||||||
script.setName(SCRIPT_CODE);
|
script.setName(SCRIPT_CODE);
|
||||||
script.setLabel("Script Source");
|
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." +
|
String scriptTemplate = "//enter your script code here";
|
||||||
"This authenticator exposes the following additional variables: 'script', 'LOG'");
|
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);
|
return asList(name, description, script);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import org.keycloak.models.ScriptModel;
|
import org.keycloak.models.ScriptModel;
|
||||||
|
@ -6,7 +22,6 @@ import javax.script.Bindings;
|
||||||
import javax.script.ScriptContext;
|
import javax.script.ScriptContext;
|
||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
import javax.script.ScriptEngineManager;
|
import javax.script.ScriptEngineManager;
|
||||||
import javax.script.ScriptException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
|
* 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;
|
private final ScriptEngineManager scriptEngineManager;
|
||||||
|
|
||||||
public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
|
public DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) {
|
||||||
|
|
||||||
|
if (scriptEngineManager == null) {
|
||||||
|
throw new IllegalStateException("scriptEngineManager must not be null!");
|
||||||
|
}
|
||||||
|
|
||||||
this.scriptEngineManager = scriptEngineManager;
|
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
|
@Override
|
||||||
public InvocableScript prepareScript(ScriptModel script) {
|
public InvocableScriptAdapter prepareInvocableScript(ScriptModel scriptModel, ScriptBindingsConfigurer bindingsConfigurer) {
|
||||||
return prepareScript(script, ScriptBindingsConfigurer.EMPTY);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (scriptModel == null) {
|
||||||
public InvocableScript prepareScript(ScriptModel script, ScriptBindingsConfigurer bindingsConfigurer) {
|
throw new IllegalArgumentException("script must not be null");
|
||||||
|
|
||||||
if (script == null) {
|
|
||||||
throw new NullPointerException("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");
|
throw new IllegalArgumentException("script must not be null or empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bindingsConfigurer == null) {
|
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);
|
throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
|
||||||
}
|
}
|
||||||
|
|
||||||
configureBindings(bindingsConfigurer, engine);
|
configureBindings(bindingsConfigurer, scriptEngine);
|
||||||
|
|
||||||
loadScriptIntoEngine(script, engine);
|
return scriptEngine;
|
||||||
|
|
||||||
return new InvocableScript(script, engine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
|
private void configureBindings(ScriptBindingsConfigurer bindingsConfigurer, ScriptEngine engine) {
|
||||||
|
@ -61,15 +106,9 @@ public class DefaultScriptingProvider implements ScriptingProvider {
|
||||||
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
|
engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadScriptIntoEngine(ScriptModel script, ScriptEngine engine) {
|
/**
|
||||||
|
* Looks-up a {@link ScriptEngine} based on the MIME-type provided by the given {@link Script}.
|
||||||
try {
|
*/
|
||||||
engine.eval(script.getCode());
|
|
||||||
} catch (ScriptException se) {
|
|
||||||
throw new ScriptExecutionException(script, se);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
|
private ScriptEngine lookupScriptEngineFor(ScriptModel script) {
|
||||||
return scriptEngineManager.getEngineByMimeType(script.getMimeType());
|
return scriptEngineManager.getEngineByMimeType(script.getMimeType());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package org.keycloak.scripting;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
@ -13,11 +29,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
|
||||||
|
|
||||||
static final String ID = "script-based-auth";
|
static final String ID = "script-based-auth";
|
||||||
|
|
||||||
private final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ScriptingProvider create(KeycloakSession session) {
|
public ScriptingProvider create(KeycloakSession session) {
|
||||||
return new DefaultScriptingProvider(scriptEngineManager);
|
return new DefaultScriptingProvider(ScriptEngineManagerHolder.SCRIPT_ENGINE_MANAGER);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -39,4 +53,12 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder class for lazy initialization of {@link ScriptEngineManager}.
|
||||||
|
*/
|
||||||
|
private static class ScriptEngineManagerHolder {
|
||||||
|
|
||||||
|
private static final ScriptEngineManager SCRIPT_ENGINE_MANAGER = new ScriptEngineManager();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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.");
|
"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-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-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-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.");
|
||||||
addProviderInfo(result, "auth-username-password-form", "Username Password Form",
|
addProviderInfo(result, "auth-username-password-form", "Username Password Form",
|
||||||
"Validates a username and password from login form.");
|
"Validates a username and password from login form.");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -2138,9 +2138,20 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
$scope.flow = flow;
|
$scope.flow = flow;
|
||||||
$scope.create = true;
|
$scope.create = true;
|
||||||
$scope.config = { config: {}};
|
|
||||||
$scope.configType = configType;
|
$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() {
|
$scope.$watch(function() {
|
||||||
return $location.path();
|
return $location.path();
|
||||||
}, function() {
|
}, function() {
|
||||||
|
@ -2165,8 +2176,6 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
|
||||||
//$location.url("/realms");
|
//$location.url("/realms");
|
||||||
window.history.back();
|
window.history.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6" data-ng-show="option.type == 'Script'">
|
<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]}}
|
{{config[option.name]}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -377,6 +377,6 @@ h1 i {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace_editor {
|
.ace_editor {
|
||||||
height: 400px;
|
height: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
Loading…
Reference in a new issue