Merge pull request #1538 from patriot1burke/master

refactor auth-spi, auth demo, and docs
This commit is contained in:
Bill Burke 2015-08-14 15:14:00 -04:00
commit 52fd2d7e75
20 changed files with 375 additions and 57 deletions

View file

@ -624,8 +624,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
<title>Enable Required Action</title> <title>Enable Required Action</title>
<para> <para>
The final thing you have to do is go into the admin console. Click on the Authentication left menu. 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 Required Actions tab. Click on the Register button and choose your new Required Action.
click on the default action checkbox, this required action will be applied anytime a new user is created. Your new required action should now be displayed and enabled in the required actions list.
</para> </para>
</section> </section>
</section> </section>
@ -637,8 +637,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
to the out of the box registration page. to the out of the box registration page.
An additional SPI was created to be able to do this. It basically allows 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 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 after the user has been registered. We'll look at both the implementation of the user profile registration
Keycloak provides out of the box to show you how to do this. processing as well as the registration Google Recaptcha plugin.
</para> </para>
<section> <section>
<title>Implementation FormAction Interface</title> <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 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 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 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[ <programlisting><![CDATA[
@Override @Override
public void buildPage(FormContext context, LoginFormsProvider form) { public void buildPage(FormContext context, LoginFormsProvider form) {
@ -667,7 +667,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
</programlisting> </programlisting>
</para> </para>
<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 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. be displayed in the HTML page generated by the registration Freemarker template.
</para> </para>
@ -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() Recaptcha also has the requirement of loading a javascript script. You can do this by calling LoginFormsProvider.addScript()
passing in the URL. passing in the URL.
</para> </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> <para>
The next meaty part of this interface is the validate() method. This is called immediately upon receiving a form 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[ <programlisting><![CDATA[
@Override @Override
public void validate(ValidationContext context) { 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. format of a form element, i.e. an alternative email attribute.
</para> </para>
<para> <para>
After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha Let's also look at the user profile plugin that is used to validate email address and other user information
this is a no-op, but if you have additional metadata you want to add to UserModel, you can do that in success() method. 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>
<para> <para>
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.
</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, 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, you are also required to define a FormActionFactory class. This class is implemented similarly to AuthenticatorFactory, so we won't go over it.
</para> </para>
</section> </section>
<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 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: of each FormActionFactory implementation you have in the jar. For example:
<programlisting> <programlisting>
org.keycloak.examples.authenticator.registration.RecaptchaFormActionFactory org.keycloak.authentication.forms.RegistrationProfile
org.keycloak.authentication.forms.RegistrationRecaptcha
</programlisting> </programlisting>
</para> </para>
<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 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. 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 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>
<para> <para>
After you've created your flow, you have to bind it to registration. If you go After you've created your flow, you have to bind it to registration. If you go

View file

@ -1,27 +1,32 @@
Example User Federation Provider Example User Federation Provider
=================================================== ===================================================
This is an example of user federation backed by a simple properties file. This properties file only contains username/password This is an example of defining a custom Authenticator and Required action. This example is explained in the user documentation
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: 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: Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
"providers": [ "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 You then have to copy the secret-question.ftl and secret-question-config.ftl files to the standalone/configuration/themes/base/login directory.
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 After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into
a "test-users.properties" within the JAR that you can use as the variable. the admin console to create a new flow with your new authenticator.
The FilePropertiesFederationProvider is an example of a writable provider. It synchronizes changes made to If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
username and password with the properties file. If you go to the Users/Federation page of the admin console you will defined flows. You cannot modify an built in flows, so, to add the Authenticator you
see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to have to copy an existing flow or create your own.
a properties file in the "path" field of the admin page for this plugin.
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.

View file

@ -10,7 +10,7 @@
<description/> <description/>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>authenticator-example</artifactId> <artifactId>authenticator-required-action-example</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
@ -42,6 +42,6 @@
</dependencies> </dependencies>
<build> <build>
<finalName>federation-properties-example</finalName> <finalName>authenticator-required-action-example</finalName>
</build> </build>
</project> </project>

View file

@ -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>

View file

@ -13,6 +13,8 @@ import org.keycloak.services.util.CookieHelper;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -24,7 +26,11 @@ public class SecretQuestionAuthenticator implements Authenticator {
protected boolean hasCookie(AuthenticationFlowContext context) { protected boolean hasCookie(AuthenticationFlowContext context) {
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED"); 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 @Override
@ -33,12 +39,17 @@ public class SecretQuestionAuthenticator implements Authenticator {
context.success(); context.success();
return; return;
} }
Response challenge = context.form().createForm("secret_question.ftl"); Response challenge = context.form().createForm("secret-question.ftl");
context.challenge(challenge); context.challenge(challenge);
} }
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.cancelLogin();
return;
}
boolean validated = validateAnswer(context); boolean validated = validateAnswer(context);
if (!validated) { if (!validated) {
Response challenge = context.form() Response challenge = context.form()
@ -58,11 +69,12 @@ public class SecretQuestionAuthenticator implements Authenticator {
maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age")); 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", CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(), uri.getRawPath(),
null, null, null, null,
maxCookieAge, maxCookieAge,
true, true); false, true);
} }
protected boolean validateAnswer(AuthenticationFlowContext context) { protected boolean validateAnswer(AuthenticationFlowContext context) {

View file

@ -20,14 +20,14 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { 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); context.challenge(challenge);
} }
@Override @Override
public void processAction(RequiredActionContext context) { public void processAction(RequiredActionContext context) {
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer")); String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer"));
UserCredentialValueModel model = new UserCredentialValueModel(); UserCredentialValueModel model = new UserCredentialValueModel();
model.setValue(answer); model.setValue(answer);
model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE); model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);

View file

@ -1117,12 +1117,15 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'AuthenticationFlowsCtrl' 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', templateUrl : resourceUrl + '/partials/create-execution.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
}, },
topFlow: function($route) {
return $route.current.params.topFlow;
},
parentFlow : function(AuthenticationFlowLoader) { parentFlow : function(AuthenticationFlowLoader) {
return AuthenticationFlowLoader(); return AuthenticationFlowLoader();
}, },
@ -1135,12 +1138,15 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'CreateExecutionCtrl' 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', templateUrl : resourceUrl + '/partials/create-flow-execution.html',
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
}, },
topFlow: function($route) {
return $route.current.params.topFlow;
},
parentFlow : function(AuthenticationFlowLoader) { parentFlow : function(AuthenticationFlowLoader) {
return AuthenticationFlowLoader(); return AuthenticationFlowLoader();
}, },
@ -1164,6 +1170,9 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
},
unregisteredRequiredActions : function(UnregisteredRequiredActionsListLoader) {
return UnregisteredRequiredActionsListLoader();
} }
}, },
controller : 'RequiredActionsCtrl' controller : 'RequiredActionsCtrl'

View file

@ -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, CreateExecutionFlow,
Notifications, $location) { Notifications, $location) {
$scope.realm = realm; $scope.realm = realm;
@ -1669,16 +1669,16 @@ module.controller('CreateExecutionFlowCtrl', function($scope, realm, parentFlow,
$scope.save = function() { $scope.save = function() {
$scope.flow.provider = $scope.provider.id; $scope.flow.provider = $scope.provider.id;
CreateExecutionFlow.save({realm: realm.realm, alias: parentFlow.alias}, $scope.flow, function() { 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."); Notifications.success("Flow Created.");
}) })
} }
$scope.cancel = function() { $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, CreateExecution,
Notifications, $location) { Notifications, $location) {
$scope.realm = realm; $scope.realm = realm;
@ -1700,12 +1700,12 @@ module.controller('CreateExecutionCtrl', function($scope, realm, parentFlow, for
provider: $scope.provider.id provider: $scope.provider.id
} }
CreateExecution.save({realm: realm.realm, alias: parentFlow.alias}, execution, function() { 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."); Notifications.success("Execution Created.");
}) })
} }
$scope.cancel = function() { $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) { $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) { $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'); console.log('RequiredActionsCtrl');
$scope.realm = realm; $scope.realm = realm;
$scope.unregisteredRequiredActions = unregisteredRequiredActions;
$scope.requiredActions = []; $scope.requiredActions = [];
var setupRequiredActionsForm = function() { var setupRequiredActionsForm = function() {
console.log('setupRequiredActionsForm'); console.log('setupRequiredActionsForm');
RequiredActions.query({id: realm.realm}, function(data) { RequiredActions.query({realm: realm.realm}, function(data) {
$scope.requiredActions = []; $scope.requiredActions = [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
$scope.requiredActions.push(data[i]); $scope.requiredActions.push(data[i]);
@ -1868,12 +1871,35 @@ module.controller('RequiredActionsCtrl', function($scope, realm, RequiredActions
}; };
$scope.updateRequiredAction = function(action) { $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"); Notifications.success("Required action updated");
setupRequiredActionsForm(); 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(); setupRequiredActionsForm();

View file

@ -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) { module.factory('RealmSessionStatsLoader', function(Loader, RealmSessionStats, $route, $q) {
return Loader.get(RealmSessionStats, function() { return Loader.get(RealmSessionStats, function() {
return { return {

View file

@ -229,7 +229,7 @@ module.factory('BruteForceUser', function($resource) {
module.factory('RequiredActions', 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', realm : '@realm',
alias : '@alias' 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) { module.factory('RealmLDAPConnectionTester', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection');
}); });

View file

@ -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>

View file

@ -4,6 +4,13 @@
<kc-tabs-authentication></kc-tabs-authentication> <kc-tabs-authentication></kc-tabs-authentication>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <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"> <tr data-ng-hide="requiredActions.length == 0">
<th>Required Action</th> <th>Required Action</th>
<th>Enabled</th> <th>Enabled</th>

View file

@ -217,4 +217,11 @@ public interface AuthenticationFlowContext {
* @return * @return
*/ */
URI getActionUrl(); URI getActionUrl();
/**
* End the flow and redirect browser based on protocol specific respones. This should only be executed
* in browser-based flows.
*
*/
void cancelLogin();
} }

View file

@ -17,6 +17,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -367,6 +368,17 @@ public class AuthenticationProcessor {
public URI getActionUrl() { public URI getActionUrl() {
return getActionUrl(generateAccessCode()); 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() { public void logFailure() {

View file

@ -54,6 +54,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return authenticationFlow.processAction(actionExecution); return authenticationFlow.processAction(actionExecution);
} else if (model.getId().equals(actionExecution)) { } else if (model.getId().equals(actionExecution)) {
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); 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()); Authenticator authenticator = factory.create(processor.getSession());
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
authenticator.action(result); authenticator.action(result);
@ -106,7 +109,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator()); AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
if (factory == null) { 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()); Authenticator authenticator = factory.create(processor.getSession());
AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId()); AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId());

View file

@ -26,13 +26,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) { if (formData.containsKey("cancel")) {
context.getEvent().error(Errors.REJECTED_BY_USER); context.cancelLogin();
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);
return; return;
} }
if (!validateForm(context, formData)) { if (!validateForm(context, formData)) {

View file

@ -433,6 +433,9 @@ public class AuthenticationManager {
for (String action : requiredActions) { for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId()); 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); RequiredActionProvider actionProvider = factory.create(session);
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory); RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context); actionProvider.requiredActionChallenge(context);
@ -508,6 +511,9 @@ public class AuthenticationManager {
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue; if (!model.isEnabled()) continue;
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId()); 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); RequiredActionProvider provider = factory.create(session);
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) { RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) {
@Override @Override

View file

@ -12,6 +12,8 @@ import org.keycloak.authentication.DefaultAuthenticationFlow;
import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormAction;
import org.keycloak.authentication.FormAuthenticationFlow; import org.keycloak.authentication.FormAuthenticationFlow;
import org.keycloak.authentication.FormAuthenticator; import org.keycloak.authentication.FormAuthenticator;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; 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") @Path("required-actions")
@GET @GET

View file

@ -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";
}
}

View file

@ -0,0 +1 @@
org.keycloak.testsuite.actions.DummyRequiredActionFactory