/* * 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.InvocableScriptAdapter; import org.keycloak.scripting.ScriptExecutionException; import org.keycloak.scripting.ScriptingProvider; import java.util.Map; /** * An {@link Authenticator} that can execute a configured script during authentication flow. * <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 clientSession} the current {@link org.keycloak.sessions.AuthenticationSessionModel}</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> * Note that the {@code user} variable is only defined when the user was identified by a preceeding * authentication step, e.g. by the {@link UsernamePasswordForm} authenticator. * </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) { * * var username = user ? user.username : "anonymous"; * LOG.info(script.name + " --> trace auth for: " + username); * * if ( 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> */ public class ScriptBasedAuthenticator implements Authenticator { private static final Logger LOGGER = Logger.getLogger(ScriptBasedAuthenticator.class); static final String SCRIPT_CODE = "scriptCode"; static final String SCRIPT_NAME = "scriptName"; static final String SCRIPT_DESCRIPTION = "scriptDescription"; static final String ACTION_FUNCTION_NAME = "action"; static final String AUTHENTICATE_FUNCTION_NAME = "authenticate"; @Override public void authenticate(AuthenticationFlowContext context) { tryInvoke(AUTHENTICATE_FUNCTION_NAME, context); } @Override public void action(AuthenticationFlowContext context) { tryInvoke(ACTION_FUNCTION_NAME, context); } private void tryInvoke(String functionName, AuthenticationFlowContext 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 (!invocableScriptAdapter.isDefined(functionName)) { return; } try { //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 boolean hasAuthenticatorConfig(AuthenticationFlowContext context) { return context != null && context.getAuthenticatorConfig() != null && context.getAuthenticatorConfig().getConfig() != null && !context.getAuthenticatorConfig().getConfig().isEmpty(); } private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) { Map<String, String> config = context.getAuthenticatorConfig().getConfig(); String scriptName = config.get(SCRIPT_NAME); String scriptCode = config.get(SCRIPT_CODE); String scriptDescription = config.get(SCRIPT_DESCRIPTION); RealmModel realm = context.getRealm(); ScriptingProvider scripting = context.getSession().getProvider(ScriptingProvider.class); //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("clientSession", context.getAuthenticationSession()); bindings.put("LOG", LOGGER); }); } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { //TODO make RequiredActions configurable in the script //NOOP } @Override public void close() { //NOOP } }