keycloak-aplcache

auth spi refactor and doco

8/13/2015 12:28:11 PM

Changes

services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java 67(+0 -67)

Details

diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index e849273..05e7f42 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -226,6 +226,9 @@ Forms Subflow - ALTERNATIVE
             <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>
@@ -293,8 +296,7 @@ Forms Subflow - ALTERNATIVE
             </para>
             <para>
                 If the hasCookie() method returns false, we must return a response that renders the secret question HTML
-                form.  If your Authenticator classes inherit from the helper class <literal>org.keycloak.authentication.AbstractFormAuthenticator</literal>
-                it has a loginForm() method that initializes a Freemarker page builder with appropriate base information needed
+                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
@@ -314,9 +316,9 @@ Forms Subflow - ALTERNATIVE
     public void action(AuthenticationFlowContext context) {
         boolean validated = validateAnswer(context);
         if (!validated) {
-           Response challenge = loginForm(context)
+           Response challenge = context.form()
                                  .setError("badSecret")
-                                 .createForm("secret_question.ftl");
+                                 .createForm("secret-question.ftl");
            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
            return;
         }
@@ -360,5 +362,165 @@ Forms Subflow - ALTERNATIVE
                 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 2814a4d..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,12 @@ 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;
@@ -19,7 +18,7 @@ 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";
 
@@ -34,7 +33,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
             context.success();
             return;
         }
-        Response challenge = loginForm(context).createForm("secret_question.ftl");
+        Response challenge = context.form().createForm("secret_question.ftl");
         context.challenge(challenge);
     }
 
@@ -42,9 +41,9 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
     public void action(AuthenticationFlowContext context) {
         boolean validated = validateAnswer(context);
         if (!validated) {
-            Response challenge =  loginForm(context)
+            Response challenge =  context.form()
                     .setError("badSecret")
-                    .createForm("secret_question.ftl");
+                    .createForm("secret-question.ftl");
             context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
             return;
         }
@@ -68,7 +67,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
 
     protected boolean validateAnswer(AuthenticationFlowContext context) {
         MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
-        String secret = formData.getFirst("secret");
+        String secret = formData.getFirst("secret_answer");
         UserCredentialValueModel cred = null;
         for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
             if (model.getType().equals(CREDENTIAL_TYPE)) {
@@ -77,7 +76,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
             }
         }
 
-        return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
+        return cred.getValue().equals(secret);
     }
 
     @Override
@@ -92,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();