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>
|
<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
|
||||||
|
|
|
@ -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.
|
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
|
After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into
|
||||||
page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you
|
the admin console to create a new flow with your new authenticator.
|
||||||
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
|
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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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.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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
<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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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