keycloak-uncached
Changes
docbook/reference/en/en-US/modules/auth-spi.xml 95(+84 -11)
examples/providers/authenticator/README.md 37(+21 -16)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java 20(+16 -4)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java 4(+2 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 48(+37 -11)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/unregistered-required-action-selector.html 21(+21 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/required-actions.html 7(+7 -0)
services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java 8(+1 -7)
services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java 46(+46 -0)
Details
docbook/reference/en/en-US/modules/auth-spi.xml 95(+84 -11)
diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index 29c8ce1..e26643e 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
<title>Enable Required Action</title>
<para>
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.
</para>
</section>
</section>
@@ -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.
</para>
<section>
<title>Implementation FormAction Interface</title>
@@ -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.
<programlisting><![CDATA[
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
@@ -667,7 +667,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
</programlisting>
</para>
<para>
- 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.
</para>
@@ -682,8 +682,12 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
passing in the URL.
</para>
<para>
+ For user profile processing, there is no additional information that it needs to add to the form, so its buildPage() method
+ is empty.
+ </para>
+ <para>
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.
<programlisting><![CDATA[
@Override
public void validate(ValidationContext context) {
@@ -721,11 +725,78 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
format of a form element, i.e. an alternative email attribute.
</para>
<para>
+ Let's also look at the user profile plugin that is used to validate email address and other user information
+ when registering.
+<programlisting><![CDATA[
+ @Override
+ public void validate(ValidationContext context) {
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ List<FormMessage> 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();
+ }
+ }]]>
+</programlisting>
+ </para>
+ <para>
+ 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.
+ </para>
+ <para>
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.
+ 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.
+<programlisting><![CDATA[
+ @Override
+ public void success(FormContext context) {
+ UserModel user = context.getUser();
+ MultivaluedMap<String, String> 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));
+ }]]>
+
+</programlisting>
+ </para>
+ <para>
+ Pretty simple implementation. The UserModel of the newly registered user is obtained from the FormContext.
+ The appropriate methods are called to initialize UserModel data.
</para>
<para>
- Finally the FormActionFactory class is really implemented similarly to AuthenticatorFactory, so we won't go over it.
+ Finally, you are also required to define a FormActionFactory class. This class is implemented similarly to AuthenticatorFactory, so we won't go over it.
</para>
</section>
<section>
@@ -735,7 +806,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
of each FormActionFactory implementation you have in the jar. For example:
<programlisting>
- org.keycloak.examples.authenticator.registration.RecaptchaFormActionFactory
+ org.keycloak.authentication.forms.RegistrationProfile
+ org.keycloak.authentication.forms.RegistrationRecaptcha
</programlisting>
</para>
<para>
@@ -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.
</para>
<para>
After you've created your flow, you have to bind it to registration. If you go
diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml
index 1e177f2..abfdfda 100755
--- a/examples/providers/authenticator/pom.xml
+++ b/examples/providers/authenticator/pom.xml
@@ -10,7 +10,7 @@
<description/>
<modelVersion>4.0.0</modelVersion>
- <artifactId>authenticator-example</artifactId>
+ <artifactId>authenticator-required-action-example</artifactId>
<packaging>jar</packaging>
<dependencies>
@@ -42,6 +42,6 @@
</dependencies>
<build>
- <finalName>federation-properties-example</finalName>
+ <finalName>authenticator-required-action-example</finalName>
</build>
</project>
examples/providers/authenticator/README.md 37(+21 -16)
diff --git a/examples/providers/authenticator/README.md b/examples/providers/authenticator/README.md
index e65a778..5a8c50a 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/secret-question-config.ftl b/examples/providers/authenticator/secret-question-config.ftl
new file mode 100755
index 0000000..54e6902
--- /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">
+ <form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
+ <div class="${properties.kcFormGroupClass!}">
+ <div class="${properties.kcLabelWrapperClass!}">
+ <label for="totp" class="${properties.kcLabelClass!}">What is your mom's first name?</label>
+ </div>
+
+ <div class="${properties.kcInputWrapperClass!}">
+ <input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
+ </div>
+ </div>
+
+ <div class="${properties.kcFormGroupClass!}">
+ <div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
+ <div class="${properties.kcFormOptionsWrapperClass!}">
+ </div>
+ </div>
+
+ <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
+ <div class="${properties.kcFormButtonsWrapperClass!}">
+ <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doSubmit")}"/>
+ </div>
+ </div>
+ </div>
+ </form>
+ </#if>
+</@layout.registrationLayout>
\ 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 1f4a8aa..f70c0f7 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -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<String, String> 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 d0e2de7..8fa26ee 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 19a3562..66bfba7 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 9aeb0e8..ef1f602 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 2c30dd3..e9cf3df 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 64a5ff5..a2e6446 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 0000000..682ae68
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/modal/unregistered-required-action-selector.html
@@ -0,0 +1,21 @@
+<div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()">
+ <span class="pficon pficon-close"></span>
+ </button>
+ <h4 class="modal-title">Register Required Action</h4>
+</div>
+<div class="modal-body">
+ <form>
+ <div>
+ <label class="control-label" for="selector">Required Action</label>
+ <select id="selector" class="form-control"
+ ng-model="selected.selected"
+ ng-options="r.name for r in unregisteredRequiredActions">
+ </select>
+ </div>
+ </form>
+</div>
+<div class="modal-footer">
+ <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
+ <button type="button" class="btn btn-primary" ng-click="ok()">Ok</button>
+</div>
\ 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 b7cd948..097fdc9 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 @@
<kc-tabs-authentication></kc-tabs-authentication>
<table class="table table-striped table-bordered">
<thead>
+ <tr data-ng-hide="unregisteredRequiredActions.length == 0">
+ <th colspan = "3" class="kc-table-actions">
+ <div class="pull-right" data-ng-show="access.manageRealm">
+ <button class="btn btn-default" data-ng-click="register()">Register</button>
+ </div>
+ </th>
+ </tr>
<tr data-ng-hide="requiredActions.length == 0">
<th>Required Action</th>
<th>Enabled</th>
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index fd9803a..26ec7bd 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 aee300e..c7c80f4 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/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index 9f42cf5..e8490ea 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<String, String> 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/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index 18f1703..49c48ad 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/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 82e0d4d..52cca07 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 b792213..cce673b 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<Map<String, String>> getUnregisteredRequiredActions() {
+ List<ProviderFactory> factories = session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class);
+ List<Map<String, String>> 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<String, String> 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<String, String> 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 0000000..7813e4d
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 0000000..31c00c8
--- /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