diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml index 29c8ce11a5..e26643e800 100755 --- a/docbook/reference/en/en-US/modules/auth-spi.xml +++ b/docbook/reference/en/en-US/modules/auth-spi.xml @@ -624,8 +624,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor Enable Required Action The final thing you have to do is go into the admin console. Click on the Authentication left menu. - Click on the Required Actions tab. Find your required action, and enable. Alternatively, if you - click on the default action checkbox, this required action will be applied anytime a new user is created. + Click on the Required Actions tab. Click on the Register button and choose your new Required Action. + Your new required action should now be displayed and enabled in the required actions list. @@ -637,8 +637,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor to the out of the box registration page. An additional SPI was created to be able to do this. It basically allows you to add validation of form elements on the page as well as to initialize UserModel attributes and data - after the user has been registered. We'll look at the implementation of the recaptcha support that - Keycloak provides out of the box to show you how to do this. + after the user has been registered. We'll look at both the implementation of the user profile registration + processing as well as the registration Google Recaptcha plugin.
Implementation FormAction Interface @@ -646,7 +646,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor The core interface you have to implement is the FormAction interface. A FormAction is responsible for rendering and processing a portion of the page. Rendering is done in the buildPage() method, validation is done in the validate() method, post validation operations are done in success(). Let's first take a look - at buildPage() + at buildPage() method of the Recaptcha plugin. - The buildPage() method is a callback by the form flow to help render the page. It receives a form parameter + The Recaptcha buildPage() method is a callback by the form flow to help render the page. It receives a form parameter which is a LoginFormsProvider. You can add additional attributes to the form provider so that they can be displayed in the HTML page generated by the registration Freemarker template. @@ -681,9 +681,13 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor Recaptcha also has the requirement of loading a javascript script. You can do this by calling LoginFormsProvider.addScript() passing in the URL. + + For user profile processing, there is no additional information that it needs to add to the form, so its buildPage() method + is empty. + The next meaty part of this interface is the validate() method. This is called immediately upon receiving a form - post. + post. Let's look at the Recaptcha's plugin first. - After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha - this is a no-op, but if you have additional metadata you want to add to UserModel, you can do that in success() method. + Let's also look at the user profile plugin that is used to validate email address and other user information + when registering. + formData = context.getHttpRequest().getDecodedFormParameters(); + List errors = new ArrayList<>(); + + String eventError = Errors.INVALID_REGISTRATION; + + if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_FIRST_NAME)))) { + errors.add(new FormMessage(RegistrationPage.FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME)); + } + + if (Validation.isBlank(formData.getFirst((RegistrationPage.FIELD_LAST_NAME)))) { + errors.add(new FormMessage(RegistrationPage.FIELD_LAST_NAME, Messages.MISSING_LAST_NAME)); + } + + String email = formData.getFirst(Validation.FIELD_EMAIL); + if (Validation.isBlank(email)) { + errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL)); + } else if (!Validation.isEmailValid(email)) { + formData.remove(Validation.FIELD_EMAIL); + errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL)); + } + + if (context.getSession().users().getUserByEmail(email, context.getRealm()) != null) { + formData.remove(Validation.FIELD_EMAIL); + errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS)); + } + + if (errors.size() > 0) { + context.validationError(formData, errors); + return; + + } else { + context.success(); + } + }]]> + - Finally the FormActionFactory class is really implemented similarly to AuthenticatorFactory, so we won't go over it. + As you can see, this validate() method of user profile processing makes sure that the email, first, and last name + are filled in in the form. It also makes sure that email is in the right format. If any of these validations + fail, an error message is queued up for rendering. Any fields in error are removed from the form data. Error messages + are represented by the FormMessage class. The first parameter of the constructor of this class takes the HTML + element id. The input in error will be highlighted when the form is re-rendered. The second parameter is + a message reference id. This id must correspond to a property in one of the localized message bundle files. + in the theme. + + + After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha + this is a no-op, so we won't go over it. For user profile processing, this method fills in values in the registered + user. + formData = context.getHttpRequest().getDecodedFormParameters(); + user.setFirstName(formData.getFirst(RegistrationPage.FIELD_FIRST_NAME)); + user.setLastName(formData.getFirst(RegistrationPage.FIELD_LAST_NAME)); + user.setEmail(formData.getFirst(RegistrationPage.FIELD_EMAIL)); + }]]> + + + + + Pretty simple implementation. The UserModel of the newly registered user is obtained from the FormContext. + The appropriate methods are called to initialize UserModel data. + + + Finally, you are also required to define a FormActionFactory class. This class is implemented similarly to AuthenticatorFactory, so we won't go over it.
@@ -735,7 +806,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor and must be contained in the META-INF/services/ directory of your jar. This file must list the fully qualified classname of each FormActionFactory implementation you have in the jar. For example: - org.keycloak.examples.authenticator.registration.RecaptchaFormActionFactory + org.keycloak.authentication.forms.RegistrationProfile + org.keycloak.authentication.forms.RegistrationRecaptcha @@ -758,7 +830,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor Basically you'll have to copy the registration flow. Then click Actions menu to the right of the Registration Form, and pick "Add Execution" to add a new execution. You'll pick the FormAction from the selection list. Make sure your FormAction comes after "Registration User Creation" by using the down errors to move it if your FormAction - isn't already listed after "Registration User Creation". + isn't already listed after "Registration User Creation". You want your FormAction to come after user creation + because the success() method of Regsitration User Creation is responsible for creating the new UserModel. After you've created your flow, you have to bind it to registration. If you go diff --git a/examples/providers/authenticator/README.md b/examples/providers/authenticator/README.md index e65a7789de..5a8c50a833 100755 --- a/examples/providers/authenticator/README.md +++ b/examples/providers/authenticator/README.md @@ -1,27 +1,32 @@ Example User Federation Provider =================================================== -This is an example of user federation backed by a simple properties file. This properties file only contains username/password -key pairs. To deploy, build this directory then take the jar and copy it to standalone/configuration/providers. Alternatively you can deploy as a module by running: +This is an example of defining a custom Authenticator and Required action. This example is explained in the user documentation +of Keycloak. To deploy, build this directory then take the jar and copy it to standalone/configuration/providers. Alternatively you can deploy as a module by running: - KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.userprops --resources=target/federation-properties-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api" + KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.secret-question --resources=target/authenticator-required-action-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api,org.keycloak.keycloak-services" Then registering the provider by editing keycloak-server.json and adding the module to the providers field: "providers": [ .... - "module:org.keycloak.examples.userprops" + "module:org.keycloak.examples.secret-question" ], - -You will then have to restart the authentication server. -The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation - page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you -specify a classpath to a properties file in the "path" field of the admin page for this plugin. This example includes -a "test-users.properties" within the JAR that you can use as the variable. - -The FilePropertiesFederationProvider is an example of a writable provider. It synchronizes changes made to -username and password with the properties file. If you go to the Users/Federation page of the admin console you will -see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to -a properties file in the "path" field of the admin page for this plugin. +You then have to copy the secret-question.ftl and secret-question-config.ftl files to the standalone/configuration/themes/base/login directory. + +After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into +the admin console to create a new flow with your new authenticator. + +If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently +defined flows. You cannot modify an built in flows, so, to add the Authenticator you +have to copy an existing flow or create your own. + +Next you have to register your required action. +Click on the Required Actions tab. Click on the Register button and choose your new Required Action. +Your new required action should now be displayed and enabled in the required actions list. + +I'm hoping the UI is intuitive enough so that you +can figure out for yourself how to create a flow and add the Authenticator and Required Action. We're looking to add a screencast +to show this in action. diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml index 1e177f2f6c..abfdfdaf7f 100755 --- a/examples/providers/authenticator/pom.xml +++ b/examples/providers/authenticator/pom.xml @@ -10,7 +10,7 @@ 4.0.0 - authenticator-example + authenticator-required-action-example jar @@ -42,6 +42,6 @@ - federation-properties-example + authenticator-required-action-example diff --git a/examples/providers/authenticator/secret-question-config.ftl b/examples/providers/authenticator/secret-question-config.ftl new file mode 100755 index 0000000000..54e69026b0 --- /dev/null +++ b/examples/providers/authenticator/secret-question-config.ftl @@ -0,0 +1,33 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("loginTitle",realm.name)} + <#elseif section = "header"> + Setup Secret Question + <#elseif section = "form"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java index 1f4a8aaa00..f70c0f7dc3 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -13,6 +13,8 @@ import org.keycloak.services.util.CookieHelper; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; /** * @author Bill Burke @@ -24,7 +26,11 @@ public class SecretQuestionAuthenticator implements Authenticator { protected boolean hasCookie(AuthenticationFlowContext context) { Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED"); - return cookie != null; + boolean result = cookie != null; + if (result) { + System.out.println("Bypassing secret question because cookie as set"); + } + return result; } @Override @@ -33,12 +39,17 @@ public class SecretQuestionAuthenticator implements Authenticator { context.success(); return; } - Response challenge = context.form().createForm("secret_question.ftl"); + Response challenge = context.form().createForm("secret-question.ftl"); context.challenge(challenge); } @Override public void action(AuthenticationFlowContext context) { + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + if (formData.containsKey("cancel")) { + context.cancelLogin(); + return; + } boolean validated = validateAnswer(context); if (!validated) { Response challenge = context.form() @@ -58,11 +69,12 @@ public class SecretQuestionAuthenticator implements Authenticator { maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age")); } + URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build(); CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true", - context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(), + uri.getRawPath(), null, null, maxCookieAge, - true, true); + false, true); } protected boolean validateAnswer(AuthenticationFlowContext context) { diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java index d0e2de7123..8fa26eec3a 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java @@ -20,14 +20,14 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider { @Override public void requiredActionChallenge(RequiredActionContext context) { - Response challenge = context.form().createForm("secret_question_config.ftl"); + Response challenge = context.form().createForm("secret-question-config.ftl"); context.challenge(challenge); } @Override public void processAction(RequiredActionContext context) { - String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer")); + String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer")); UserCredentialValueModel model = new UserCredentialValueModel(); model.setValue(answer); model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index 19a3562ccb..66bfba7961 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1117,12 +1117,15 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'AuthenticationFlowsCtrl' }) - .when('/realms/:realm/authentication/flows/:flow/create/execution', { + .when('/realms/:realm/authentication/flows/:flow/create/execution/:topFlow', { templateUrl : resourceUrl + '/partials/create-execution.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, + topFlow: function($route) { + return $route.current.params.topFlow; + }, parentFlow : function(AuthenticationFlowLoader) { return AuthenticationFlowLoader(); }, @@ -1135,12 +1138,15 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'CreateExecutionCtrl' }) - .when('/realms/:realm/authentication/flows/:flow/create/flow/execution', { + .when('/realms/:realm/authentication/flows/:flow/create/flow/execution/:topFlow', { templateUrl : resourceUrl + '/partials/create-flow-execution.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, + topFlow: function($route) { + return $route.current.params.topFlow; + }, parentFlow : function(AuthenticationFlowLoader) { return AuthenticationFlowLoader(); }, @@ -1164,6 +1170,9 @@ module.config([ '$routeProvider', function($routeProvider) { resolve : { realm : function(RealmLoader) { return RealmLoader(); + }, + unregisteredRequiredActions : function(UnregisteredRequiredActionsListLoader) { + return UnregisteredRequiredActionsListLoader(); } }, controller : 'RequiredActionsCtrl' diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 9aeb0e8a73..ef1f60296f 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1651,7 +1651,7 @@ module.controller('CreateFlowCtrl', function($scope, realm, }; }); -module.controller('CreateExecutionFlowCtrl', function($scope, realm, parentFlow, formProviders, +module.controller('CreateExecutionFlowCtrl', function($scope, realm, topFlow, parentFlow, formProviders, CreateExecutionFlow, Notifications, $location) { $scope.realm = realm; @@ -1669,16 +1669,16 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, parentFlow, $scope.save = function() { $scope.flow.provider = $scope.provider.id; CreateExecutionFlow.save({realm: realm.realm, alias: parentFlow.alias}, $scope.flow, function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); Notifications.success("Flow Created."); }) } $scope.cancel = function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); }; }); -module.controller('CreateExecutionCtrl', function($scope, realm, parentFlow, formActionProviders, authenticatorProviders, +module.controller('CreateExecutionCtrl', function($scope, realm, topFlow, parentFlow, formActionProviders, authenticatorProviders, CreateExecution, Notifications, $location) { $scope.realm = realm; @@ -1700,12 +1700,12 @@ module.controller('CreateExecutionCtrl', function($scope, realm, parentFlow, for provider: $scope.provider.id } CreateExecution.save({realm: realm.realm, alias: parentFlow.alias}, execution, function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); Notifications.success("Execution Created."); }) } $scope.cancel = function() { - $location.url("/realms/" + realm.realm + "/authentication/flows/" + parentFlow.alias); + $location.url("/realms/" + realm.realm + "/authentication/flows/" + topFlow); }; }); @@ -1793,12 +1793,12 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo } $scope.addSubFlow = function(execution) { - $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/flow/execution'); + $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/flow/execution/' + $scope.flow.alias); } $scope.addSubFlowExecution = function(execution) { - $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/execution'); + $location.url("/realms/" + realm.realm + '/authentication/flows/' + execution.flowId + '/create/execution/' + $scope.flow.alias); } @@ -1853,13 +1853,16 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo }); -module.controller('RequiredActionsCtrl', function($scope, realm, RequiredActions, Notifications) { +module.controller('RequiredActionsCtrl', function($scope, realm, unregisteredRequiredActions, + $modal, $route, + RegisterRequiredAction, RequiredActions, Notifications) { console.log('RequiredActionsCtrl'); $scope.realm = realm; + $scope.unregisteredRequiredActions = unregisteredRequiredActions; $scope.requiredActions = []; var setupRequiredActionsForm = function() { console.log('setupRequiredActionsForm'); - RequiredActions.query({id: realm.realm}, function(data) { + RequiredActions.query({realm: realm.realm}, function(data) { $scope.requiredActions = []; for (var i = 0; i < data.length; i++) { $scope.requiredActions.push(data[i]); @@ -1868,12 +1871,35 @@ module.controller('RequiredActionsCtrl', function($scope, realm, RequiredActions }; $scope.updateRequiredAction = function(action) { - RequiredActions.update({id: realm.realm, alias: action.alias}, action, function() { + RequiredActions.update({realm: realm.realm, alias: action.alias}, action, function() { Notifications.success("Required action updated"); setupRequiredActionsForm(); }); } + $scope.register = function() { + var controller = function($scope, $modalInstance) { + $scope.unregisteredRequiredActions = unregisteredRequiredActions; + $scope.selected = { + selected: $scope.unregisteredRequiredActions[0] + } + $scope.ok = function () { + $modalInstance.close(); + RegisterRequiredAction.save({realm: realm.realm}, $scope.selected.selected); + $route.reload(); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + } + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/unregistered-required-action-selector.html', + controller: controller, + resolve: { + } + }); + } + setupRequiredActionsForm(); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js index 2c30dd3646..e9cf3dfc18 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js @@ -73,6 +73,14 @@ module.factory('RequiredActionsListLoader', function(Loader, RequiredActions, $r }); }); +module.factory('UnregisteredRequiredActionsListLoader', function(Loader, UnregisteredRequiredActions, $route, $q) { + return Loader.query(UnregisteredRequiredActions, function() { + return { + realm : $route.current.params.realm + } + }); +}); + module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $route, $q) { return Loader.get(RealmSessionStats, function() { return { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 64a5ff5727..a2e6446412 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -229,7 +229,7 @@ module.factory('BruteForceUser', function($resource) { module.factory('RequiredActions', function($resource) { - return $resource(authUrl + '/admin/realms/:id/authentication/required-actions/:alias', { + return $resource(authUrl + '/admin/realms/:realm/authentication/required-actions/:alias', { realm : '@realm', alias : '@alias' }, { @@ -239,6 +239,18 @@ module.factory('RequiredActions', function($resource) { }); }); +module.factory('UnregisteredRequiredActions', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/authentication/unregistered-required-actions', { + realm : '@realm' + }); +}); + +module.factory('RegisterRequiredAction', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/authentication/register-required-action', { + realm : '@realm' + }); +}); + module.factory('RealmLDAPConnectionTester', function($resource) { return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); }); diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/unregistered-required-action-selector.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/unregistered-required-action-selector.html new file mode 100755 index 0000000000..682ae68e3f --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/unregistered-required-action-selector.html @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html index b7cd948d54..097fdc9c84 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html @@ -4,6 +4,13 @@ + + + diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index fd9803af39..26ec7bd862 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -217,4 +217,11 @@ public interface AuthenticationFlowContext { * @return */ URI getActionUrl(); + + /** + * End the flow and redirect browser based on protocol specific respones. This should only be executed + * in browser-based flows. + * + */ + void cancelLogin(); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index aee300e1db..c7c80f4c1f 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -17,6 +17,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.AuthenticationManager; @@ -367,6 +368,17 @@ public class AuthenticationProcessor { public URI getActionUrl() { return getActionUrl(generateAccessCode()); } + + @Override + public void cancelLogin() { + getEvent().error(Errors.REJECTED_BY_USER); + LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod()); + protocol.setRealm(getRealm()) + .setHttpHeaders(getHttpRequest().getHttpHeaders()) + .setUriInfo(getUriInfo()); + Response response = protocol.cancelLogin(getClientSession()); + forceChallenge(response); + } } public void logFailure() { diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index 18f17030a9..49c48ad543 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -54,6 +54,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { return authenticationFlow.processAction(actionExecution); } else if (model.getId().equals(actionExecution)) { AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); + if (factory == null) { + throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?"); + } Authenticator authenticator = factory.create(processor.getSession()); AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); authenticator.action(result); @@ -106,7 +109,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); if (factory == null) { - throw new AuthenticationFlowException("Could not find AuthenticatorFactory for: " + model.getAuthenticator(), AuthenticationFlowError.INTERNAL_ERROR); + throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?"); } Authenticator authenticator = factory.create(processor.getSession()); AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java index 9f42cf56cf..e8490eab7c 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java @@ -26,13 +26,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl public void action(AuthenticationFlowContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("cancel")) { - context.getEvent().error(Errors.REJECTED_BY_USER); - LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod()); - protocol.setRealm(context.getRealm()) - .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) - .setUriInfo(context.getUriInfo()); - Response response = protocol.cancelLogin(context.getClientSession()); - context.forceChallenge(response); + context.cancelLogin(); return; } if (!validateForm(context, formData)) { diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 82e0d4d881..52cca074fb 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -433,6 +433,9 @@ public class AuthenticationManager { for (String action : requiredActions) { RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId()); + if (factory == null) { + throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?"); + } RequiredActionProvider actionProvider = factory.create(session); RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory); actionProvider.requiredActionChallenge(context); @@ -508,6 +511,9 @@ public class AuthenticationManager { for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { if (!model.isEnabled()) continue; RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId()); + if (factory == null) { + throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?"); + } RequiredActionProvider provider = factory.create(session); RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) { @Override diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index b7922134dc..cce673bb76 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -12,6 +12,8 @@ import org.keycloak.authentication.DefaultAuthenticationFlow; import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAuthenticationFlow; import org.keycloak.authentication.FormAuthenticator; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; @@ -682,6 +684,50 @@ public class AuthenticationManagementResource { } } + @Path("unregistered-required-actions") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public List> getUnregisteredRequiredActions() { + List factories = session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class); + List> unregisteredList = new LinkedList<>(); + for (ProviderFactory factory : factories) { + RequiredActionFactory requiredActionFactory = (RequiredActionFactory) factory; + boolean found = false; + for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { + if (model.getProviderId().equals(factory.getId())) { + found = true; + break; + } + } + if (!found) { + Map data = new HashMap<>(); + data.put("name", requiredActionFactory.getDisplayText()); + data.put("providerId", requiredActionFactory.getId()); + unregisteredList.add(data); + } + + } + return unregisteredList; + } + + @Path("register-required-action") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @NoCache + public void registereRequiredAction(Map data) { + String providerId = data.get("providerId"); + String name = data.get("name"); + RequiredActionProviderModel requiredAction = new RequiredActionProviderModel(); + requiredAction.setAlias(providerId); + requiredAction.setName(name); + requiredAction.setProviderId(providerId); + requiredAction.setDefaultAction(false); + requiredAction.setEnabled(true); + realm.addRequiredActionProvider(requiredAction); + } + + @Path("required-actions") @GET diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java new file mode 100755 index 0000000000..7813e4d03e --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/DummyRequiredActionFactory.java @@ -0,0 +1,43 @@ +package org.keycloak.testsuite.actions; + +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class DummyRequiredActionFactory implements RequiredActionFactory { + @Override + public String getDisplayText() { + return "Dummy Action"; + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return null; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "dummy-action"; + } +} diff --git a/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory new file mode 100755 index 0000000000..31c00c8575 --- /dev/null +++ b/testsuite/integration/src/test/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -0,0 +1 @@ +org.keycloak.testsuite.actions.DummyRequiredActionFactory \ No newline at end of file
+
+ +
+
Required Action Enabled