keycloak-uncached
Changes
docbook/reference/en/en-US/modules/auth-spi.xml 446(+446 -0)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java 75(+48 -27)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java 56(+29 -27)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java 47(+47 -0)
examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java 48(+48 -0)
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java 10(+5 -5)
services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java 7(+1 -6)
services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java 2(+1 -1)
Details
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 29a49a7..7a798f8 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -46,6 +46,7 @@
<!ENTITY CustomAttributes SYSTEM "modules/custom-attributes.xml">
<!ENTITY ProtocolMappers SYSTEM "modules/protocol-mappers.xml">
<!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
+ <!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
]>
<book>
@@ -140,6 +141,7 @@ This one is short
&Proxy;
&CustomAttributes;
&ProtocolMappers;
+ &AuthSPI;
&Migration;
</book>
docbook/reference/en/en-US/modules/auth-spi.xml 446(+446 -0)
diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index 54bd36d..05e7f42 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -74,7 +74,453 @@
</para>
</listitem>
</varlistentry>
+ <varlistentry>
+ <term>Required Action</term>
+ <listitem>
+ <para>
+ After authentication completes, the user might have one or more one-time actions he must
+ complete before he is allowed to login. The user might be required to set up an OTP token
+ generator or reset an expired password or even accept a Terms and Conditions document.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
</section>
+ <section>
+ <title>Algorithm Overview</title>
+ <para>
+ Let's talk about how this all works for browser login. Let's assume the following flows, executions and sub flows.
+<programlisting><![CDATA[
+Cookie - ALTERNATIVE
+Kerberos - ALTERNATIVE
+Forms Subflow - ALTERNATIVE
+ Username/Password Form - REQUIRED
+ OTP Password Form - OPTIONAL
+]]>
+</programlisting>
+ </para>
+ <para>
+ In the top level of the form we have 3 executions of which all are alternatively required. This means that
+ if any of these are successful, then the others do not have to execute. The Username/Password form is not executed
+ if there is an SSO Cookie set or a successful Kerberos login. Let's walk through the steps from when a client
+ first redirects to keycloak to authenticate the user.
+ <orderedlist>
+ <listitem>
+ <para>
+ The OpenID Connect or SAML protocol provider unpacks relevent data, verifies the client and any signatures.
+ It creates a ClientSessionModel. It looks up what the browser flow should be, then starts executing the flow.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The flow looks at the cookie execution and sees that it is an alternative. It loads the cookie provider.
+ It checks to see if the cookie provider requires that a user already be associated with the client session.
+ Cookie provider does not require a user. If it did, the flow would abort and the user would see an error screen.
+ Cookie provider then executes. Its purpose is to see if there is an SSO cookie set. If there is one set, it is validated
+ and the UserSessionModel is verified and associated with the ClientSessionModel. The Cookie provider returns a
+ success() status if the SSO cookie exists and is validated. Since the cookie provider returned success and each execution
+ at this level of the flow is ALTERNATIVE, no other execution is executed and this results in a successful login.
+ If there is no SSO cookie, the cookie provider returns with a status of attempted(). This means there was no error condition,
+ but no success either. The provider tried, but the request just wasn't set up to handle this authenticator.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Next the flow looks at the Kerberos execution. This is also an alternative. The kerberos provider also does not
+ require a user to be already set up and associated with the ClientSessionModel so this provider is executed.
+ Kerberos uses the SPNEGO browser protocol. This requires a series of challenge/responses between the server and client
+ exchanging negotiation headers. The kerberos provider does not see any negotiate header, so it assumes that this is the
+ first interaction between the server and client. It therefore creates an HTTP challenge response to the client and sets a
+ forceChallenge() status. A forceChallenge() means that this HTTP response cannot be ignored by the flow and must be returned to the
+ client. If instead the provider returned a challenge() status, the flow would hold the challenge response until all other alternatives
+ are attempted. So, in this initial phase, the flow would stop and the challenge response would be sent back to the browser. If the browser
+ then responds with a successful negotiate header, the provider associates the user with the ClientSession and the flow ends because
+ the rest of the executions on this level of the flow are all alternatives. Otherwise, again, the kerberos provider
+ sets an attempted() status and the flow continues.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The next execution is a subflow called Forms. The executions for this subflow are loaded and
+ the same processing logic occurs
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The first execution in the Forms subflow is the UsernamePassword provider. This provider also does not require for a user
+ to already be associated with the flow. This provider creates challenge HTTP response and sets its status to challenge().
+ This execution is required, so the flow honors this challenge and sends the HTTP response back to the browser. This
+ response is a rendering of the Username/Password HTML page. The user enters in their username and password and clicks submit.
+ This HTTP request is directed to the UsernamePassword provider. If the user entered an invalid username or password, a new
+ challenge response is created and a status of failureChallenge() is set for this execution. A failureChallenge() means that
+ there is a challenge, but that the flow should log this as an error in the error log. This error log can be used to lock accounts
+ or IP Addresses that have had too many login failures. If the username and password is valid, the provider associated the
+ UserModel with the ClientSessionModel and returns a status of success()
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ The next execution is the OTP Form. This provider requires that a user has been associated with the flow. This requirement is satisfied
+ because the UsernamePassword provider already associated the user with the flow. Since a user is required for this provider, the provider
+ is also asked if the user is configured to use this provider. If user is not configured, and this execution is required, then the flow will
+ then set up a required action that the user must perform after authentication is complete. For OTP, this means the OTP setup page.
+ If the execution was optional, then this execution is skipped.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ After the flow is complete, the authentication processor creates a UserSEssionModel and associates it with the ClientSEssionModel.
+ It then checks to see if the user is required to complete any required actions before logging in.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ First, each required action's evaluateTriggers() method is called. This allows the required action provider to figure out if
+ there is some state that might trigger the action to be fired. For example, if your realm has a password expiration policy,
+ it might be triggered by this method.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Each required action associated with the user that has its requiredActionChallenge() method called. Here the provider
+ sets up an HTTP response which renders the page for the required action. This is done by setting a challenge status.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ If the required action is ultimately successful, then the required action is removed from the user's require actions list.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ After all required actions have been resolved, the user is finally logged in.
+ </para>
+ </listitem>
+ </orderedlist>
+ </para>
+ </section>
+ <section>
+ <title>Authenticator SPI Walk Through</title>
+ <para>
+ In this section, we'll take a look at the Authenticator interface. For this, we are going to implement an authenticator
+ that requires that a user enter in the answer to a secret question like "What is your mother's maiden name?". This example
+ is fully implemented and contained in the examples/providers/authenticator directory of the demo distribution of Keycloak.
+ </para>
+ <para>
+ The classes you must implement are the org.keycloak.authentication.AuthenticatorFactory and Authenticator interfaces. The Authenticator
+ interface defines the logic. The AuthenticatorFactory is responsible for creating instances of an Authenticator. They both extend
+ a more generic Provider and ProviderFactory set of interfaces that other Keycloak components like User Federation do.
+ </para>
+ <section>
+ <title>Packaging Classes and Deployment</title>
+ <para>
+ You will package your classes within a single jar. This jar must contain a file named <literal>org.keycloak.authentication.AuthenticatorFactory</literal>
+ 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 AuthenticatorFactory implementation you have in the jar. For example:
+ <programlisting>
+ org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
+ org.keycloak.examples.authenticator.AnotherProviderFactory
+ </programlisting>
+ </para>
+ <para>
+ This services/ file is used by Keycloak to scan the providers it has to load into the system.
+ </para>
+ <para>
+ To deploy this jar, just copy it to the standalone/configuration/providers directory.
+ </para>
+ </section>
+ <section>
+ <title>Implementing an Authenticator</title>
+ <para>
+ When implementing the Authenticator interface, the first method that needs to be implemented is the
+ requiresUser() method. For our example, this method must return true as we need to validate the secret question
+ associated with the user. A provider like kerberos would return false from this method as it can
+ resolve a user from the negotiate header. This example however is validating a specific credential of a specific
+ user.
+ </para>
+ <para>
+ The next method to implement is the configuredFor() method. This method is responsible for determining if the
+ user is configured for this particular authenticator. For this example, we need to check of the answer to the
+ secret question is been set up by the user or not. In our case we are storing this information, hashed, within
+ a UserCredentialValueModel within the UserModel (just like passwords are stored). Here's how we do this
+ very simple check:
+<programlisting>
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return session.users().configuredForCredentialType("secret_question", realm, user);
+ }
+</programlisting>
+ </para>
+ <para>
+ The configuredForCredentialType() call queries the user to see if it supports that credential type.
+ </para>
+ <para>
+ The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns fales
+ and our example authenticator is required within the flow, this method will be called. It is response for
+ registering any required actions that must be performed by the user. In our example, we need to register
+ a required action that will force the user to set up the answer to the secret question. We will implement
+ this required action provider later in this chapter. Here is the implementation of the setRequiredActions()
+ method.
+<programlisting>
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ user.addRequiredAction("SECRET_QUESTION_CONFIG");
+ }
+</programlisting>
+ </para>
+ <para>
+ Now we are getting into the meat of the Authenticator implementation. The next method to implement is
+ authenticate(). This is the initial method the flow invokes when the execution is first visited. What we
+ want is that if a user has answered the secret question already on their browser's machine, that the
+ user doesn't have to answer the question again. Basically making that machine "trusted". The authenticate()
+ method isn't responsible for processing the secret question form. Its sole purpose is to render the page
+ or to continue the flow.
+<programlisting>
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ if (hasCookie(context)) {
+ context.success();
+ return;
+ }
+ Response challenge = loginForm(context).createForm("secret_question.ftl");
+ context.challenge(challenge);
+ }
+</programlisting>
+ </para>
+ <para>
+ If the hasCookie() method checks to see if there is already a cookie set on the browser which indicates
+ that the secret question has already been answered. If that returns true, we just mark this execution's
+ status as SUCCESS using the AuthenticationFlowContext.success() method and returning from the authentication()
+ method.
+ </para>
+ <para>
+ If the hasCookie() method returns false, we must return a response that renders the secret question HTML
+ form. AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed
+ to build the form. This page builder is called <literal>org.keycloak.login.LoginFormsProvider</literal>.
+ the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme. Additionally
+ you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the
+ Freemarker template. We'll go over this later.
+ </para>
+ <para>
+ Calling LoginFormsProvider.createForm() returns a JAX-RS Response object. We then call AuthenticationFlowContext.challenge()
+ passing in this response. This sets the status of the execution as CHALLENGE and if the execution is Required, this
+ JAX-RS Response object will be sent to the browser.
+ </para>
+ <para>
+ So, the HTML page asking for the answer to a secret question is displayed to the user and the user
+ enteres in the answer and clicks submit. The action URL of the HTML form will send an HTTP request to the
+ flow. The flow will end up invoking the action() method of our Authenticator implementation.
+<programlisting>
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ boolean validated = validateAnswer(context);
+ if (!validated) {
+ Response challenge = context.form()
+ .setError("badSecret")
+ .createForm("secret-question.ftl");
+ context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
+ return;
+ }
+ setCookie(context);
+ context.success();
+ }
+
+
+</programlisting>
+ </para>
+ <para>
+ If the answer is not valid, we rebuild the HTML Form with an additional error message. We then call
+ AuthenticationFlowContext.failureChallenge() passing in the reason for the value and the JAX-RS response.
+ failureChallenge() works the same as challenge(), but it also records the failure so it can be analyzed
+ by any attack detection service.
+ </para>
+ <para>
+ If validation is successful, then we set a cookie to remember that the secret question has been answered
+ and we call AuthenticationFlowContext.success().
+ </para>
+ <para>
+ The last thing I want to go over is the setCookie() method. This is an example of providing configuration
+ for the Authenticator. In this case we want the max age of the cookie to be configurable.
+<programlisting>
+ protected void setCookie(AuthenticationFlowContext context) {
+ AuthenticatorConfigModel config = context.getAuthenticatorConfig();
+ int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
+ if (config != null) {
+ maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
+
+ }
+ ... set the cookie ...
+ }
+
+</programlisting>
+ </para>
+ <para>
+ We obtain an AuthenticatorConfigModel from the AuthenticationFlowContext.getAuthenticatorConfig() method.
+ If configuration exists we pull the max age config out of it. We will see how we can define what should
+ be configured when we talk about the AuthenticatorFactory implementation. The config values can be
+ defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
+ </para>
+ </section>
+ <section>
+ <title>Implementing an AuthenticatorFactory</title>
+ <para>
+ The next step in this process is to implement an AuthenticatorFactory. This factory is responsible
+ for instantiating an Authenticator. It also provides deployment and configuration metadata about
+ the Authenticator.
+ </para>
+ <para>
+ The getId() method is just the unique name of the component. The create() methods should also
+ be self explanatory. The create(KeycloakSession) method will actually never be called. It is just
+ an artifact of the more generic ProviderFactory interface.
+<programlisting>
+public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "secret-question-authenticator";
+ private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public Authenticator create() {
+ return SINGLETON;
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+</programlisting>
+ </para>
+ <para>
+ The next thing the factory is responsible for is specify the allowed requirement switches. While there
+ are four different requirement types: ALTERNATIVE, REQUIRED, OPTIONAL, DISABLED, AuthenticatorFactory
+ implementations can limit which requirement options are shown in the admin console when defining
+ a flow. In our example, we're going to limit our requirement options to REQUIRED and DISABLED.
+<programlisting>
+ private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ AuthenticationExecutionModel.Requirement.DISABLED
+ };
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+</programlisting>
+ </para>
+ <para>
+ The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not
+ Authenticator.setRequiredActions() method will be called. If an Authenticator is not configured for a user,
+ the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error. If it
+ returns true, then the flow manager will invoke Authenticator.setRequiredActions().
+<programlisting>
+ @Override
+ public boolean isUserSetupAllowed() {
+ return true;
+ }
+</programlisting>
+ </para>
+ <para>
+ The next few methods define how the Authenticator can be configured. The isConfigurable() method
+ is a flag which specifies to the admin console on whether the Authenticator can be configured within
+ a flow. The getConfigProperties() method returns a list of ProviderConfigProperty objects. These
+ objects define a specific configuration attribute.
+<programlisting><![CDATA[
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
+ }
+
+ private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName("cookie.max.age");
+ property.setLabel("Cookie Max Age");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
+ configProperties.add(property);
+ }
+]]></programlisting>
+ </para>
+ <para>
+ Each ProviderConfigProperty defines the name of the config property. This is the key used in the config
+ map stored in AuthenticatorConfigModel. The label defines how the config option will be displayed in the
+ admin console. The type defines if it is a String, Boolean, or other type. The admin console
+ will display different UI inputs depending on the type. The help text is what will be shown in the
+ tooltip for the config attribute in the admin console. Read the javadoc of ProviderConfigProperty
+ for more detail.
+ </para>
+ <para>
+ The rest of the methods are for the admin console. getHelpText() is the tooltip text that will be
+ shown when you are picking the Authenticator you want to bind to an execution. getDisplayType()
+ is what text that will be shown in the admin console when listing the Authenticator. getReferenceCategory()
+ is just a category the Authenticator belongs to.
+ </para>
+ </section>
+ <section>
+ <title>Adding Authenticator Form</title>
+ <para>
+ Keycloak comes with a Freemarker <link linkend="themes">theme and template engine</link>. The createForm()
+ method you called within authenticate() of your Authenticator class, builds an HTML page from a file within
+ your login them: secret-question.ftl. This file should be placed in the login theme with all the other
+ .ftl files you see for login.
+ </para>
+ <para>
+ Let's take a bigger look at secret-question.ftl Here's a small code snippet:
+<programlisting><![CDATA[
+ <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!}">${msg("loginSecretQuestion")}</label>
+ </div>
+
+ <div class="${properties.kcInputWrapperClass!}">
+ <input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
+ </div>
+ </div>
+
+]]></programlisting>
+ </para>
+ <para>
+ Any piece of text enclosed in <literal>${}</literal> corresponds to an attribute or template funtion.
+ If you see the form's action, you see it points to <literal>${url.loginAction}</literal>. This value
+ is automatically generated when you invoke the AuthenticationFlowContext.form() method. You can also obtain
+ this value by calling the AuthenticationFlowContext.getActionURL() method in Java code.
+ </para>
+ <para>
+ You'll also see <literal>${properties.someValue}</literal>. These correspond to properties defined
+ in your theme.properties file of our theme. <literal>${msg("someValue")}</literal> corresponds to the
+ internationalized message bundles (.properties files) included with the login theme messages/ directory.
+ If you're just using english, you can just add the value of the <literal>loginSecretQuestion</literal>
+ value. This should be the question you want to ask the user.
+ </para>
+ <para>
+ When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance. If you called,
+ <literal>LoginFormsProvider.setAttribute("foo", "bar")</literal>, the value of "foo" would be available
+ for reference in your form as <literal>${foo}</literal>. The value of an attribute can be any Java
+ bean as well.
+ </para>
+ </section>
+ <section>
+ <title>Adding Authenticator to a Flow</title>
+ <para>
+ Adding an Authenticator to a flow must be done in the admin console.
+ 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 we've created you
+ have to copy an existing flow or create your own. 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.
+ </para>
+ <para>
+ After you've created your flow, you have to bind it to the login action you want to bind it to. If you go
+ to the Authentication menu and go to the Bindings tab you will see options to bind a flow to
+ the browser, registration, or direct grant flow.
+ </para>
+ </section>
+ </section>
</chapter>
\ No newline at end of file
diff --git a/examples/providers/authenticator/secret-question.ftl b/examples/providers/authenticator/secret-question.ftl
new file mode 100755
index 0000000..b69c520
--- /dev/null
+++ b/examples/providers/authenticator/secret-question.ftl
@@ -0,0 +1,34 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "title">
+ ${msg("loginTitle",realm.name)}
+ <#elseif section = "header">
+ ${msg("loginTitleHtml",realm.name)}
+ <#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("doLogIn")}"/>
+ <input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
+ </div>
+ </div>
+ </div>
+ </form>
+ </#if>
+</@layout.registrationLayout>
\ No newline at end of file
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
index 8e60d9b..1f4a8aa 100755
--- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
@@ -2,13 +2,15 @@ package org.keycloak.examples.authenticator;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.AbstractFormAuthenticator;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.utils.CredentialValidation;
+import org.keycloak.services.util.CookieHelper;
+import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@@ -16,25 +18,56 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
+public class SecretQuestionAuthenticator implements Authenticator {
public static final String CREDENTIAL_TYPE = "secret_question";
+ protected boolean hasCookie(AuthenticationFlowContext context) {
+ Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
+ return cookie != null;
+ }
+
@Override
public void authenticate(AuthenticationFlowContext context) {
- Response challenge = loginForm(context).createForm("secret_question.ftl");
+ if (hasCookie(context)) {
+ context.success();
+ return;
+ }
+ Response challenge = context.form().createForm("secret_question.ftl");
context.challenge(challenge);
}
@Override
public void action(AuthenticationFlowContext context) {
- MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
- String secret = formData.getFirst("secret");
- if (secret == null || secret.trim().equals("")) {
- badSecret(context);
+ boolean validated = validateAnswer(context);
+ if (!validated) {
+ Response challenge = context.form()
+ .setError("badSecret")
+ .createForm("secret-question.ftl");
+ context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return;
}
+ setCookie(context);
+ context.success();
+ }
+
+ protected void setCookie(AuthenticationFlowContext context) {
+ AuthenticatorConfigModel config = context.getAuthenticatorConfig();
+ int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
+ if (config != null) {
+ maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
+
+ }
+ CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
+ context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(),
+ null, null,
+ maxCookieAge,
+ true, true);
+ }
+ protected boolean validateAnswer(AuthenticationFlowContext context) {
+ MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+ String secret = formData.getFirst("secret_answer");
UserCredentialValueModel cred = null;
for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
if (model.getType().equals(CREDENTIAL_TYPE)) {
@@ -42,25 +75,8 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
break;
}
}
- if (cred == null) {
- badSecret(context);
- return;
- }
- boolean validated = CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
- if (!validated) {
- badSecret(context);
- return;
- }
-
- context.success();
- }
-
- private void badSecret(AuthenticationFlowContext context) {
- Response challenge = loginForm(context)
- .setError("badSecret")
- .createForm("secret_question.ftl");
- context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
+ return cred.getValue().equals(secret);
}
@Override
@@ -75,6 +91,11 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
- user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG");
+ user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
+ }
+
+ @Override
+ public void close() {
+
}
}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
index 09a83da..09be4c1 100755
--- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
@@ -22,28 +22,18 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
@Override
- public Authenticator create() {
- return SINGLETON;
+ public String getId() {
+ return PROVIDER_ID;
}
@Override
- public Authenticator create(KeycloakSession session) {
+ public Authenticator create() {
return SINGLETON;
}
@Override
- public String getDisplayType() {
- return "Secret Question";
- }
-
- @Override
- public String getReferenceCategory() {
- return "Secret Question";
- }
-
- @Override
- public boolean isConfigurable() {
- return true;
+ public Authenticator create(KeycloakSession session) {
+ return SINGLETON;
}
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
@@ -61,8 +51,13 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
}
@Override
- public String getHelpText() {
- return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
+ public boolean isConfigurable() {
+ return true;
+ }
+
+ @Override
+ public List<ProviderConfigProperty> getConfigProperties() {
+ return configProperties;
}
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@@ -70,17 +65,27 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
- property.setName("remember_machine");
- property.setLabel("Remember machine");
- property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
- property.setHelpText("If set to true, a checkbox will appear when entering in secret on whether the user wants keycloak to remember the machine. If the user wants to remember, then a persistent cookie is set, and the user will not have to enter in their secret again.");
+ property.setName("cookie.max.age");
+ property.setLabel("Cookie Max Age");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
configProperties.add(property);
}
@Override
- public List<ProviderConfigProperty> getConfigProperties() {
- return configProperties;
+ public String getHelpText() {
+ return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Secret Question";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "Secret Question";
}
@Override
@@ -98,8 +103,5 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
}
- @Override
- public String getId() {
- return PROVIDER_ID;
- }
+
}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java
new file mode 100755
index 0000000..6644b8e
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java
@@ -0,0 +1,47 @@
+package org.keycloak.examples.authenticator;
+
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.models.UserCredentialValueModel;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SecretQuestionRequiredAction implements RequiredActionProvider {
+ public static final String PROVIDER_ID = "secret_question_config";
+
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+
+ }
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ 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"));
+ UserCredentialValueModel model = new UserCredentialValueModel();
+ model.setValue(answer);
+ model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
+ context.getUser().updateCredentialDirectly(model);
+ context.success();
+ }
+
+ @Override
+ public String getProviderId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java
new file mode 100755
index 0000000..ff699e2
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java
@@ -0,0 +1,48 @@
+package org.keycloak.examples.authenticator;
+
+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 SecretQuestionRequiredActionFactory implements RequiredActionFactory {
+
+ private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction();
+
+ @Override
+ public RequiredActionProvider create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+
+ @Override
+ public String getId() {
+ return SecretQuestionRequiredAction.PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayText() {
+ return "Secret Question";
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
index b085eeb..e472fff 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
@@ -6,9 +6,6 @@
${msg("loginTitleHtml",realm.name)}
<#elseif section = "form">
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
- <input id="username" name="username" value="${login.username!''}" type="hidden" />
- <input id="password-token" name="password-token" value="${login.passwordToken!''}" type="hidden" />
-
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label>
diff --git a/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java
index a318029..1755563 100755
--- a/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java
@@ -1,13 +1,5 @@
package org.keycloak.authentication;
-import org.keycloak.OAuth2Constants;
-import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.login.LoginFormsProvider;
-import org.keycloak.services.resources.LoginActionsService;
-
-import java.net.URI;
-
/**
* Abstract helper class that Authenticator implementations can leverage
*
@@ -15,53 +7,9 @@ import java.net.URI;
* @version $Revision: 1 $
*/
public abstract class AbstractFormAuthenticator implements Authenticator {
- public static final String EXECUTION = "execution";
@Override
public void close() {
}
-
- /**
- * Create a form builder that presets the user, action URI, and a generated access code
- *
- * @param context
- * @return
- */
- protected LoginFormsProvider loginForm(AuthenticationFlowContext context) {
- String accessCode = context.generateAccessCode();
- URI action = getActionUrl(context, accessCode);
- LoginFormsProvider provider = context.getSession().getProvider(LoginFormsProvider.class)
- .setUser(context.getUser())
- .setActionUri(action)
- .setClientSessionCode(accessCode);
- if (context.getForwardedErrorMessage() != null) {
- provider.setError(context.getForwardedErrorMessage());
- }
- return provider;
- }
-
- /**
- * Get the action URL for the required action.
- *
- * @param context
- * @param code client sessino access code
- * @return
- */
- public URI getActionUrl(AuthenticationFlowContext context, String code) {
- return LoginActionsService.authenticationFormProcessor(context.getUriInfo())
- .queryParam(OAuth2Constants.CODE, code)
- .queryParam(EXECUTION, context.getExecution().getId())
- .build(context.getRealm().getName());
- }
-
- /**
- * Get the action URL for the required action. This auto-generates the access code.
- *
- * @param context
- * @return
- */
- public URI getActionUrl(AuthenticationFlowContext context) {
- return getActionUrl(context, context.generateAccessCode());
- }
}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index de69493..fd9803a 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -3,6 +3,7 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder;
+import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientSessionModel;
@@ -14,6 +15,7 @@ import org.keycloak.services.managers.BruteForceProtector;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import java.net.URI;
/**
* This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set
@@ -193,4 +195,26 @@ public interface AuthenticationFlowContext {
* @return may return null if there was no error
*/
AuthenticationFlowError getError();
+
+ /**
+ * Create a Freemarker form builder that presets the user, action URI, and a generated access code
+ *
+ * @return
+ */
+ LoginFormsProvider form();
+
+ /**
+ * Get the action URL for the required action.
+ *
+ * @param code client session access code
+ * @return
+ */
+ URI getActionUrl(String code);
+
+ /**
+ * Get the action URL for the required action. This auto-generates the access code.
+ *
+ * @return
+ */
+ URI getActionUrl();
}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 552eec8..aee300e 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -3,10 +3,12 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
+import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel;
@@ -21,10 +23,12 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.util.Time;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import java.net.URI;
import java.util.List;
/**
@@ -336,6 +340,33 @@ public class AuthenticationProcessor {
public AuthenticationFlowError getError() {
return error;
}
+
+ @Override
+ public LoginFormsProvider form() {
+ String accessCode = generateAccessCode();
+ URI action = getActionUrl(accessCode);
+ LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
+ .setUser(getUser())
+ .setActionUri(action)
+ .setClientSessionCode(accessCode);
+ if (getForwardedErrorMessage() != null) {
+ provider.setError(getForwardedErrorMessage());
+ }
+ return provider;
+ }
+
+ @Override
+ public URI getActionUrl(String code) {
+ return LoginActionsService.authenticationFormProcessor(getUriInfo())
+ .queryParam(OAuth2Constants.CODE, code)
+ .queryParam("execution", getExecution().getId())
+ .build(getRealm().getName());
+ }
+
+ @Override
+ public URI getActionUrl() {
+ return getActionUrl(generateAccessCode());
+ }
}
public void logFailure() {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
index 5a96553..062d0ba 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java
@@ -36,29 +36,29 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
}
protected Response invalidUser(AuthenticationFlowContext context) {
- return loginForm(context)
+ return context.form()
.setError(Messages.INVALID_USER)
.createLogin();
}
protected Response disabledUser(AuthenticationFlowContext context) {
- return loginForm(context)
+ return context.form()
.setError(Messages.ACCOUNT_DISABLED).createLogin();
}
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
- return loginForm(context)
+ return context.form()
.setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin();
}
protected Response invalidCredentials(AuthenticationFlowContext context) {
- return loginForm(context)
+ return context.form()
.setError(Messages.INVALID_USER).createLogin();
}
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
context.getEvent().error(eventError);
- Response challengeResponse = loginForm(context)
+ Response challengeResponse = context.form()
.setError(loginFormError).createLogin();
context.failureChallenge(authenticatorError, challengeResponse);
return challengeResponse;
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
index 6f910e9..e3427e0 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java
@@ -14,7 +14,6 @@ import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
-import java.net.URI;
import java.util.LinkedList;
import java.util.List;
@@ -63,11 +62,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
}
protected Response challenge(AuthenticationFlowContext context, String error) {
- String accessCode = context.generateAccessCode();
- URI action = getActionUrl(context, accessCode);
- LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
- .setActionUri(action)
- .setClientSessionCode(accessCode);
+ LoginFormsProvider forms = context.form();
if (error != null) forms.setError(error);
return forms.createLoginTotp();
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
index bad2fa2..da2b0ae 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java
@@ -117,7 +117,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple
*/
protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) {
String accessCode = context.generateAccessCode();
- URI action = getActionUrl(context, accessCode);
+ URI action = context.getActionUrl(accessCode);
StringBuilder builder = new StringBuilder();
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
index 4719780..9f42cf5 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java
@@ -71,7 +71,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
}
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
- LoginFormsProvider forms = loginForm(context);
+ LoginFormsProvider forms = context.form();
if (formData.size() > 0) forms.setFormData(formData);
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index 9735ed1..9b650c8 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
@@ -3,6 +3,7 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder;
+import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -11,6 +12,7 @@ import org.keycloak.models.UserSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import java.net.URI;
/**
* Interface that encapsulates current information about the current requred action
@@ -19,8 +21,6 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $
*/
public interface RequiredActionContext {
- void ignore();
-
public static enum Status {
CHALLENGE,
SUCCESS,
@@ -29,6 +29,37 @@ public interface RequiredActionContext {
}
/**
+ * Get the action URL for the required action.
+ *
+ * @param code client sessino access code
+ * @return
+ */
+ URI getActionUrl(String code);
+
+ /**
+ * Get the action URL for the required action. This auto-generates the access code.
+ *
+ * @return
+ */
+ URI getActionUrl();
+
+ /**
+ * Create a Freemarker form builder that presets the user, action URI, and a generated access code
+ *
+ * @return
+ */
+ LoginFormsProvider form();
+
+
+ /**
+ * If challenge has been sent this returns the JAX-RS Response
+ *
+ * @return
+ */
+ Response getChallenge();
+
+
+ /**
* Current event builder being used
*
* @return
@@ -59,7 +90,29 @@ public interface RequiredActionContext {
Status getStatus();
+ /**
+ * Send a challenge Response back to user
+ *
+ * @param response
+ */
void challenge(Response response);
+
+ /**
+ * Abort the authentication with an error
+ *
+ */
void failure();
+
+ /**
+ * Mark this required action as successful. The required action will be removed from the UserModel
+ *
+ */
void success();
+
+ /**
+ * Ignore this required action and go onto the next, or complete the flow.
+ *
+ */
+ void ignore();
+
}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
index 10a8a0d..279b25c 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -2,16 +2,20 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
import org.keycloak.events.EventBuilder;
+import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
+import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -27,11 +31,12 @@ public class RequiredActionContextResult implements RequiredActionContext {
protected Response challenge;
protected HttpRequest httpRequest;
protected UserModel user;
+ protected RequiredActionProvider provider;
public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
HttpRequest httpRequest,
- UserModel user) {
+ UserModel user, RequiredActionProvider provider) {
this.userSession = userSession;
this.clientSession = clientSession;
this.realm = realm;
@@ -39,6 +44,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
this.session = session;
this.httpRequest = httpRequest;
this.user = user;
+ this.provider = provider;
}
@Override
@@ -121,6 +127,34 @@ public class RequiredActionContextResult implements RequiredActionContext {
status = Status.IGNORE;
}
+ @Override
+ public URI getActionUrl(String code) {
+ return LoginActionsService.requiredActionProcessor(getUriInfo())
+ .queryParam(OAuth2Constants.CODE, code)
+ .queryParam("action", provider.getProviderId())
+ .build(getRealm().getName());
+ }
+
+ @Override
+ public URI getActionUrl() {
+ String accessCode = generateAccessCode(provider.getProviderId());
+ return getActionUrl(accessCode);
+
+ }
+
+ @Override
+ public LoginFormsProvider form() {
+ String accessCode = generateAccessCode(provider.getProviderId());
+ URI action = getActionUrl(accessCode);
+ LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
+ .setUser(getUser())
+ .setActionUri(action)
+ .setClientSessionCode(accessCode);
+ return provider;
+ }
+
+
+ @Override
public Response getChallenge() {
return challenge;
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
index 627c80f..e3ecc9d 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -4,7 +4,6 @@ import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.authentication.AbstractFormRequiredAction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -14,7 +13,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class TermsAndConditions extends AbstractFormRequiredAction implements RequiredActionFactory {
+public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions";
@@ -54,7 +53,7 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
@Override
public void requiredActionChallenge(RequiredActionContext context) {
- Response challenge = form(context).createForm("terms.ftl");
+ Response challenge = context.form().createForm("terms.ftl");
context.challenge(challenge);
}
@@ -73,4 +72,8 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
return "Terms and Conditions";
}
+ @Override
+ public void close() {
+
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 9e5cd97..bd50ff0 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -423,7 +423,6 @@ public class AuthenticationManager {
evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
- RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user);
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
@@ -433,6 +432,7 @@ public class AuthenticationManager {
for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
+ RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, actionProvider);
actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
@@ -501,32 +501,33 @@ public class AuthenticationManager {
}
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
- RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user) {
- @Override
- public void challenge(Response response) {
- throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
- }
-
- @Override
- public void failure() {
- throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
- }
-
- @Override
- public void success() {
- throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
- }
-
- @Override
- public void ignore() {
- throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
- }
- };
// see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
+ RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, provider) {
+ @Override
+ public void challenge(Response response) {
+ throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
+ }
+
+ @Override
+ public void failure() {
+ throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
+ }
+
+ @Override
+ public void success() {
+ throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
+ }
+
+ @Override
+ public void ignore() {
+ throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
+ }
+ };
+
provider.evaluateTriggers(result);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index e178939..a907237 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -883,7 +883,7 @@ public class LoginActionsService {
initEvent(clientSession);
- RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser()) {
+ RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), provider) {
@Override
public String generateAccessCode(String action) {
String clientSessionAction = clientSession.getAction();