Merge pull request #1538 from patriot1burke/master
refactor auth-spi, auth demo, and docs
This commit is contained in:
commit
52fd2d7e75
20 changed files with 375 additions and 57 deletions
|
@ -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>
|
||||
|
@ -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.
|
||||
</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>
|
||||
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.
|
||||
<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>
|
||||
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>
|
||||
</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
|
||||
|
|
|
@ -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.
|
||||
You then have to copy the secret-question.ftl and secret-question-config.ftl files to the standalone/configuration/themes/base/login directory.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
|
@ -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>
|
||||
|
|
33
examples/providers/authenticator/secret-question-config.ftl
Executable file
33
examples/providers/authenticator/secret-question-config.ftl
Executable 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>
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.testsuite.actions.DummyRequiredActionFactory
|
Loading…
Reference in a new issue