keycloak-memoizeit

Merge pull request #1532 from patriot1burke/master refactor

8/11/2015 2:52:07 PM

Changes

Details

diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
new file mode 100755
index 0000000..54bd36d
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -0,0 +1,80 @@
+<chapter id="auth_spi">
+    <title>Custom Authentication, Registration, and Required Actions</title>
+    <para>
+        Keycloak comes out of the box with a bunch of different authentication mechanisms: kerberos, password, and otp.
+        These mechanisms may not meet all of your requirements and you may want to plug in your own custom ones.  Keycloak
+        provides an authentication SPI that you can use to write new plugins.  The admin console supports applying, ordering,
+        and configuring these new mechanisms.
+    </para>
+    <para>
+        Keycloak also supports a simple registration form.  Different aspects of this form can be enabled and disabled i.e.
+        Recaptcha support can be turned off and on.  The same authentication SPI can be used to add another page to the
+        registration flow or reimplement it entirely.  There's also an additional fine-grain SPI you can use to add
+        specific validations and user extensions to the built in registration form.
+    </para>
+    <para>
+        A required action in Keycloak is an action that a user has to perform after he authenticates.  After the action
+        is performed successfully, the user doesn't have to perform the action again.  Keycloak comes with some built in
+        required actions like "reset password".  This action forces the user to change their password after they have logged in.
+        You can write and plug in your own required actions.
+    </para>
+    <section>
+        <title>Terms</title>
+        <para>
+            To first learn about the Authentication SPI, let's go over some of the terms used to describe it.
+            <variablelist>
+                <varlistentry>
+                    <term>Authentication Flow</term>
+                    <listitem>
+                        <para>
+                            A flow is a container for all authentications that must happen during login or registration.  If you
+                            go to the admin console authentication page, you can view all the defined flows in the system and
+                            what authenticators they are made up of.  Flows can contain other flows.  You can also bind a new
+                            different flow for browser login, direct granta access, and registration.
+                        </para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry>
+                    <term>Authenticator</term>
+                    <listitem>
+                        <para>
+                            An authenticator is a pluggable component that hold the logic for performing the authentication
+                            or action within a flow.  It is usually a singleton.
+                        </para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry>
+                    <term>Execution</term>
+                    <listitem>
+                        <para>
+                            An execution is an object that binds the authenticator to the flow and the authenticator
+                            to the configuration of the authenticator.  Flows contain execution entries.
+                        </para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry>
+                    <term>Execution Requirement</term>
+                    <listitem>
+                        <para>
+                            Each execution defines how an authenticator behaves in a flow.  The requirement defines
+                            whether the authenticator is enabled, disabled, optional, required, or an alternative.  An
+                            alternative requirement means that the authentiactor is optional unless no other alternative
+                            authenticator is successful in the flow.  For example, cookie authentication, kerberos,
+                            and the set of all login forms are all alternative.  If one of those is successful, none of
+                            the others are executed.
+                        </para>
+                    </listitem>
+                </varlistentry>
+                <varlistentry>
+                    <term>Authenticator Config</term>
+                    <listitem>
+                        <para>
+                            This object defines the configuration for the Authenticator for a specific execution within
+                            an authentication flow.  Each execution can have a different config.
+                        </para>
+                    </listitem>
+                </varlistentry>
+            </variablelist>
+        </para>
+    </section>
+</chapter>
\ No newline at end of file
diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index 679b670..468d852 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -4,7 +4,7 @@ package org.keycloak.events;
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public interface Details {
-
+    String CUSTOM_REQUIRED_ACTION="custom_required_action";
     String EMAIL = "email";
     String PREVIOUS_EMAIL = "previous_email";
     String UPDATED_EMAIL = "updated_email";
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index d8c0d19..b049b2a 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -66,7 +66,8 @@ public enum EventType {
     IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false),
     IDENTITY_PROVIDER_ACCCOUNT_LINKING(false),
     IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false),
-    IMPERSONATE(true);
+    IMPERSONATE(true),
+    CUSTOM_REQUIRED_ACTION(true);
 
     private boolean saveByDefault;
 
diff --git a/examples/providers/authenticator/pom.xml b/examples/providers/authenticator/pom.xml
new file mode 100755
index 0000000..1e177f2
--- /dev/null
+++ b/examples/providers/authenticator/pom.xml
@@ -0,0 +1,47 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <parent>
+        <artifactId>keycloak-examples-providers-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.5.0.Final-SNAPSHOT</version>
+    </parent>
+
+    <name>Authenticator Example</name>
+    <description/>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>authenticator-example</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-model-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jboss.logging</groupId>
+            <artifactId>jboss-logging</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-services</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-login-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>federation-properties-example</finalName>
+    </build>
+</project>
diff --git a/examples/providers/authenticator/README.md b/examples/providers/authenticator/README.md
new file mode 100755
index 0000000..e65a778
--- /dev/null
+++ b/examples/providers/authenticator/README.md
@@ -0,0 +1,27 @@
+Example User Federation Provider
+===================================================
+
+This is an example of user federation backed by a simple properties file.  This properties file only contains username/password
+key pairs.  To deploy, build this directory then take the jar and copy it to standalone/configuration/providers. Alternatively you can deploy as a module by running:
+
+    KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.userprops --resources=target/federation-properties-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api"
+
+Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
+
+    "providers": [
+        ....
+        "module:org.keycloak.examples.userprops"
+    ],
+
+  
+You will then have to restart the authentication server.
+
+The ClasspathPropertiesFederationProvider is an example of a readonly provider.  If you go to the Users/Federation
+  page of the admin console you will see this provider listed under "classpath-properties.  To configure this provider you 
+specify a classpath to a properties file in the "path" field of the admin page for this plugin.  This example includes
+a "test-users.properties" within the JAR that you can use as the variable.
+  
+The FilePropertiesFederationProvider is an example of a writable provider.  It synchronizes changes made to
+username and password with the properties file.  If you go to the Users/Federation page of the admin console you will 
+see this provider listed under "file-properties".  To configure this provider you specify a fully qualified file path to 
+a properties file in the "path" field of the admin page for this plugin.  
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
new file mode 100755
index 0000000..8e60d9b
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
@@ -0,0 +1,80 @@
+package org.keycloak.examples.authenticator;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.AbstractFormAuthenticator;
+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 javax.ws.rs.core.MultivaluedMap;
+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 static final String CREDENTIAL_TYPE = "secret_question";
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        Response challenge = loginForm(context).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);
+            return;
+        }
+
+        UserCredentialValueModel cred = null;
+        for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
+            if (model.getType().equals(CREDENTIAL_TYPE)) {
+                cred = model;
+                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);
+    }
+
+    @Override
+    public boolean requiresUser() {
+        return true;
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return session.users().configuredForCredentialType(CREDENTIAL_TYPE, realm, user);
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+        user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG");
+    }
+}
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
new file mode 100755
index 0000000..09a83da
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
@@ -0,0 +1,105 @@
+package org.keycloak.examples.authenticator;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "secret-question-authenticator";
+    private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
+
+    @Override
+    public Authenticator create() {
+        return SINGLETON;
+    }
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Secret Question";
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return "Secret Question";
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return true;
+    }
+
+    private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED,
+            AuthenticationExecutionModel.Requirement.DISABLED
+    };
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return true;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
+    }
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    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.");
+        configProperties.add(property);
+    }
+
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+}
diff --git a/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
new file mode 100755
index 0000000..a62922e
--- /dev/null
+++ b/examples/providers/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -0,0 +1 @@
+org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
\ No newline at end of file
diff --git a/examples/providers/pom.xml b/examples/providers/pom.xml
index fc13539..7311514 100755
--- a/examples/providers/pom.xml
+++ b/examples/providers/pom.xml
@@ -17,5 +17,6 @@
         <module>event-listener-sysout</module>
         <module>event-store-mem</module>
         <module>federation-provider</module>
+        <module>authenticator</module>
     </modules>
 </project>
diff --git a/forms/common-themes/src/main/resources/theme/base/login/terms.ftl b/forms/common-themes/src/main/resources/theme/base/login/terms.ftl
index 991e5ed..58f4445 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/terms.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/terms.ftl
@@ -8,9 +8,9 @@
     <div id="kc-terms-text">
         ${msg("termsText")}
     </div>
-    <form class="form-actions" action="${requiredActionUrl("terms_and_conditions", "")}" method="POST">
-        <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-login" type="submit" value="${msg("doAccept")}"/>
-        <input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doDecline")}"/>
+    <form class="form-actions" action="${url.loginAction}" method="POST">
+        <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}"/>
+        <input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-decline" type="submit" value="${msg("doDecline")}"/>
     </form>
     </#if>
 </@layout.registrationLayout>
\ No newline at end of file
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index cad0c1a..745e395 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -25,7 +25,7 @@ public interface LoginFormsProvider extends Provider {
 
     public Response createResponse(UserModel.RequiredAction action);
 
-    Response createForm(String form, Map<String, Object> attributes);
+    Response createForm(String form);
 
     public Response createLogin();
 
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 245e625..12c27d4 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -278,7 +278,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
     }
 
     @Override
-    public Response createForm(String form, Map<String, Object> extraAttributes) {
+    public Response createForm(String form) {
 
         RealmModel realm = session.getContext().getRealm();
         ClientModel client = session.getContext().getClient();
diff --git a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
index d1eef51..3411f59 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/CredentialValidation.java
@@ -38,29 +38,34 @@ public class CredentialValidation {
      * @return
      */
     public static boolean validPassword(RealmModel realm, UserModel user, String password) {
-        boolean validated = false;
         UserCredentialValueModel passwordCred = null;
         for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
             if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
-                validated = new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations());
                 passwordCred = cred;
             }
         }
+        if (passwordCred == null) return false;
+
+        return validateHashedCredential(realm, user, password, passwordCred);
+
+    }
+
+    public static boolean validateHashedCredential(RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
+        boolean validated = new Pbkdf2PasswordEncoder(credential.getSalt()).verify(unhashedCredValue, credential.getValue(), credential.getHashIterations());
         if (validated) {
             int iterations = hashIterations(realm);
-            if (iterations > -1 && iterations != passwordCred.getHashIterations()) {
+            if (iterations > -1 && iterations != credential.getHashIterations()) {
                 UserCredentialValueModel newCred = new UserCredentialValueModel();
-                newCred.setType(passwordCred.getType());
-                newCred.setDevice(passwordCred.getDevice());
-                newCred.setSalt(passwordCred.getSalt());
+                newCred.setType(credential.getType());
+                newCred.setDevice(credential.getDevice());
+                newCred.setSalt(credential.getSalt());
                 newCred.setHashIterations(iterations);
-                newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(password, iterations));
+                newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(unhashedCredValue, iterations));
                 user.updateCredentialDirectly(newCred);
             }
 
         }
         return validated;
-
     }
 
     public static boolean validPasswordToken(RealmModel realm, UserModel user, String encodedPasswordToken) {
diff --git a/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java
new file mode 100755
index 0000000..a318029
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java
@@ -0,0 +1,67 @@
+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
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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/AbstractFormRequiredAction.java b/services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java
new file mode 100755
index 0000000..ea1443d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java
@@ -0,0 +1,67 @@
+package org.keycloak.authentication;
+
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.services.resources.LoginActionsService;
+
+import java.net.URI;
+
+/**
+ * Abstract helper class that Authenticator implementations can leverage
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractFormRequiredAction implements RequiredActionProvider {
+
+    /**
+     * Get the action URL for the required action.
+     *
+     * @param context
+     * @param code client sessino access code
+     * @return
+     */
+    public URI getActionUrl(RequiredActionContext context, String code) {
+        return LoginActionsService.requiredActionProcessor(context.getUriInfo())
+                .queryParam(OAuth2Constants.CODE, code)
+                .queryParam("action", getProviderId())
+                .build(context.getRealm().getName());
+    }
+
+    /**
+     * Get the action URL for the required action.  This auto-generates the access code.
+     *
+     * @param context
+     * @return
+     */
+    public URI getActionUrl(RequiredActionContext context) {
+        String accessCode = context.generateAccessCode(getProviderId());
+        return getActionUrl(context, accessCode);
+
+    }
+
+    /**
+     * Create a form builder that presets the user, action URI, and a generated access code
+     *
+     * @param context
+     * @return
+     */
+    public LoginFormsProvider form(RequiredActionContext context) {
+        String accessCode = context.generateAccessCode(getProviderId());
+        URI action = getActionUrl(context, accessCode);
+        LoginFormsProvider provider = context.getSession().getProvider(LoginFormsProvider.class)
+                .setUser(context.getUser())
+                .setActionUri(action)
+                .setClientSessionCode(accessCode);
+        return provider;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index 6a683f3..de69493 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -16,6 +16,10 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
 /**
+ * This interface encapsulates information about an execution in an AuthenticationFlow.  It is also used to set
+ * the status of the execution being performed.
+ *
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
@@ -121,6 +125,7 @@ public interface AuthenticationFlowContext {
 
     AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
 
+
     /**
      * Mark the current execution as successful.  The flow will then continue
      *
@@ -174,4 +179,18 @@ public interface AuthenticationFlowContext {
      *
      */
     void attempted();
+
+    /**
+     * Get the current status of the current execution.
+     *
+     * @return may return null if not set yet.
+     */
+    FlowStatus getStatus();
+
+    /**
+     * Get the error condition of a failed execution.
+     *
+     * @return may return null if there was no error
+     */
+    AuthenticationFlowError getError();
 }
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 3c16552..552eec8 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -3,7 +3,7 @@ package org.keycloak.authentication;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.ClientConnection;
-import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
@@ -53,16 +53,6 @@ public class AuthenticationProcessor {
     protected boolean userSessionCreated;
 
 
-    public static enum Status {
-        SUCCESS,
-        CHALLENGE,
-        FORCE_CHALLENGE,
-        FAILURE_CHALLENGE,
-        FAILED,
-        ATTEMPTED
-
-    }
-
     public RealmModel getRealm() {
         return realm;
     }
@@ -173,7 +163,7 @@ public class AuthenticationProcessor {
         AuthenticatorConfigModel authenticatorConfig;
         AuthenticationExecutionModel execution;
         Authenticator authenticator;
-        Status status;
+        FlowStatus status;
         Response challenge;
         AuthenticationFlowError error;
         List<AuthenticationExecutionModel> currentExecutions;
@@ -219,32 +209,33 @@ public class AuthenticationProcessor {
             return authenticator;
         }
 
-        public Status getStatus() {
+        @Override
+        public FlowStatus getStatus() {
             return status;
         }
 
         @Override
         public void success() {
-            this.status = Status.SUCCESS;
+            this.status = FlowStatus.SUCCESS;
         }
 
         @Override
         public void failure(AuthenticationFlowError error) {
-            status = Status.FAILED;
+            status = FlowStatus.FAILED;
             this.error = error;
 
         }
 
         @Override
         public void challenge(Response challenge) {
-            this.status = Status.CHALLENGE;
+            this.status = FlowStatus.CHALLENGE;
             this.challenge = challenge;
 
         }
 
         @Override
         public void forceChallenge(Response challenge) {
-            this.status = Status.FORCE_CHALLENGE;
+            this.status = FlowStatus.FORCE_CHALLENGE;
             this.challenge = challenge;
 
         }
@@ -252,7 +243,7 @@ public class AuthenticationProcessor {
         @Override
         public void failureChallenge(AuthenticationFlowError error, Response challenge) {
             this.error = error;
-            this.status = Status.FAILURE_CHALLENGE;
+            this.status = FlowStatus.FAILURE_CHALLENGE;
             this.challenge = challenge;
 
         }
@@ -260,14 +251,14 @@ public class AuthenticationProcessor {
         @Override
         public void failure(AuthenticationFlowError error, Response challenge) {
             this.error = error;
-            this.status = Status.FAILED;
+            this.status = FlowStatus.FAILED;
             this.challenge = challenge;
 
         }
 
         @Override
         public void attempted() {
-            this.status = Status.ATTEMPTED;
+            this.status = FlowStatus.ATTEMPTED;
 
         }
 
@@ -341,6 +332,7 @@ public class AuthenticationProcessor {
             return challenge;
         }
 
+        @Override
         public AuthenticationFlowError getError() {
             return error;
         }
@@ -348,7 +340,7 @@ public class AuthenticationProcessor {
 
     public void logFailure() {
         if (realm.isBruteForceProtected()) {
-            String username = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
+            String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
             // todo need to handle non form failures
             if (username == null) {
 
@@ -513,7 +505,7 @@ public class AuthenticationProcessor {
 
     public void attachSession() {
         String username = clientSession.getAuthenticatedUser().getUsername();
-        String attemptedUsername = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
+        String attemptedUsername = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
         if (attemptedUsername != null) username = attemptedUsername;
         if (userSession == null) { // if no authenticator attached a usersession
             boolean remember = "true".equals(clientSession.getNote(Details.REMEMBER_ME));
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 8a61476..6f910e9 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
@@ -22,7 +22,7 @@ import java.util.List;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator {
+public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
     public static final String TOTP_FORM_ACTION = "totp";
 
     @Override
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 3e341c9..bad2fa2 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
@@ -25,7 +25,7 @@ import java.util.Map;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Authenticator{
+public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator{
     public static final String KERBEROS_DISABLED = "kerberos_disabled";
     protected static Logger logger = Logger.getLogger(SpnegoAuthenticator.class);
 
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 d1922d3..4719780 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
@@ -20,7 +20,7 @@ import javax.ws.rs.core.Response;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class UsernamePasswordForm extends AbstractFormAuthenticator implements Authenticator {
+public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator implements Authenticator {
 
    @Override
     public void action(AuthenticationFlowContext context) {
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
index 1c39fff..d7fe1fe 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java
@@ -3,7 +3,7 @@ package org.keycloak.authentication.authenticators.directgrant;
 import org.jboss.logging.Logger;
 import org.keycloak.authentication.AuthenticationFlowError;
 import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.models.AuthenticationExecutionModel;
@@ -40,7 +40,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
             return;
         }
         context.getEvent().detail(Details.USERNAME, username);
-        context.getClientSession().setNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME, username);
+        context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
 
         UserModel user = null;
         try {
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
index f03f708..1df2545 100755
--- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java
@@ -149,13 +149,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
 
     public Response processResult(AuthenticationProcessor.Result result) {
         AuthenticationExecutionModel execution = result.getExecution();
-        AuthenticationProcessor.Status status = result.getStatus();
-        if (status == AuthenticationProcessor.Status.SUCCESS) {
+        FlowStatus status = result.getStatus();
+        if (status == FlowStatus.SUCCESS) {
             AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
             processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
             if (execution.isAlternative()) alternativeSuccessful = true;
             return null;
-        } else if (status == AuthenticationProcessor.Status.FAILED) {
+        } else if (status == FlowStatus.FAILED) {
             AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
             processor.logFailure();
             processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
@@ -163,10 +163,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
                 return sendChallenge(result, execution);
             }
             throw new AuthenticationFlowException(result.getError());
-        } else if (status == AuthenticationProcessor.Status.FORCE_CHALLENGE) {
+        } else if (status == FlowStatus.FORCE_CHALLENGE) {
             processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
             return sendChallenge(result, execution);
-        } else if (status == AuthenticationProcessor.Status.CHALLENGE) {
+        } else if (status == FlowStatus.CHALLENGE) {
             AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
             if (execution.isRequired()) {
                 processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
@@ -184,12 +184,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
                 processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
             }
             return null;
-        } else if (status == AuthenticationProcessor.Status.FAILURE_CHALLENGE) {
+        } else if (status == FlowStatus.FAILURE_CHALLENGE) {
             AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
             processor.logFailure();
             processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
             return sendChallenge(result, execution);
-        } else if (status == AuthenticationProcessor.Status.ATTEMPTED) {
+        } else if (status == FlowStatus.ATTEMPTED) {
             AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
             if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
                 throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
diff --git a/services/src/main/java/org/keycloak/authentication/FlowStatus.java b/services/src/main/java/org/keycloak/authentication/FlowStatus.java
new file mode 100755
index 0000000..0acd875
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/FlowStatus.java
@@ -0,0 +1,47 @@
+package org.keycloak.authentication;
+
+/**
+ * Status of an execution/authenticator in a Authentication Flow
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public enum FlowStatus {
+    /**
+     * Successful execution
+     */
+    SUCCESS,
+
+    /**
+     * Execution offered a challenge.  Optional executions will ignore this challenge.  Alternative executions may
+     * ignore the challenge depending on the status of other executions in the flow.
+     *
+     */
+    CHALLENGE,
+
+    /**
+     * Irregardless of the execution's requirement, this challenge will be sent to the user.
+     *
+     */
+    FORCE_CHALLENGE,
+
+    /**
+     * Flow will be aborted and a Response provided by the execution will be sent.
+     *
+     */
+    FAILURE_CHALLENGE,
+
+    /**
+     * Flow will be aborted.
+     *
+     */
+    FAILED,
+
+    /**
+     * This is not an error condition.  Execution was attempted, but the authenticator is unable to process the request.  An example of this is if
+     * a Kerberos authenticator did not see a negotiate header.  There was no error, but the execution was attempted.
+     *
+     */
+    ATTEMPTED
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
index 67404e5..9735ed1 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java
@@ -9,6 +9,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 
 /**
@@ -18,6 +19,15 @@ import javax.ws.rs.core.UriInfo;
  * @version $Revision: 1 $
  */
 public interface RequiredActionContext {
+    void ignore();
+
+    public static enum Status {
+        CHALLENGE,
+        SUCCESS,
+        IGNORE,
+        FAILURE
+    }
+
     /**
      * Current event builder being used
      *
@@ -46,4 +56,10 @@ public interface RequiredActionContext {
      * @return
      */
     String generateAccessCode(String action);
+
+    Status getStatus();
+
+    void challenge(Response response);
+    void failure();
+    void success();
 }
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
new file mode 100755
index 0000000..10a8a0d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java
@@ -0,0 +1,127 @@
+package org.keycloak.authentication;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.ClientConnection;
+import org.keycloak.events.EventBuilder;
+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 javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RequiredActionContextResult implements RequiredActionContext {
+    protected UserSessionModel userSession;
+    protected ClientSessionModel clientSession;
+    protected RealmModel realm;
+    protected EventBuilder eventBuilder;
+    protected KeycloakSession session;
+    protected Status status;
+    protected Response challenge;
+    protected HttpRequest httpRequest;
+    protected UserModel user;
+
+    public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
+                                       RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
+                                       HttpRequest httpRequest,
+                                       UserModel user) {
+        this.userSession = userSession;
+        this.clientSession = clientSession;
+        this.realm = realm;
+        this.eventBuilder = eventBuilder;
+        this.session = session;
+        this.httpRequest = httpRequest;
+        this.user = user;
+    }
+
+    @Override
+    public EventBuilder getEvent() {
+        return eventBuilder;
+    }
+
+    @Override
+    public UserModel getUser() {
+        return user;
+    }
+
+    @Override
+    public RealmModel getRealm() {
+        return realm;
+    }
+
+    @Override
+    public ClientSessionModel getClientSession() {
+        return clientSession;
+    }
+
+    @Override
+    public UserSessionModel getUserSession() {
+        return userSession;
+    }
+
+    @Override
+    public ClientConnection getConnection() {
+        return session.getContext().getConnection();
+    }
+
+    @Override
+    public UriInfo getUriInfo() {
+        return session.getContext().getUri();
+    }
+
+    @Override
+    public KeycloakSession getSession() {
+        return session;
+    }
+
+    @Override
+    public HttpRequest getHttpRequest() {
+        return httpRequest;
+    }
+
+    @Override
+    public String generateAccessCode(String action) {
+        ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
+        code.setAction(action);
+        return code.getCode();
+    }
+
+    @Override
+    public Status getStatus() {
+        return status;
+    }
+
+    @Override
+    public void challenge(Response response) {
+        status = Status.CHALLENGE;
+        challenge = response;
+
+    }
+
+    @Override
+    public void failure() {
+        status = Status.FAILURE;
+    }
+
+    @Override
+    public void success() {
+        status = Status.SUCCESS;
+
+    }
+
+    @Override
+    public void ignore() {
+        status = Status.IGNORE;
+    }
+
+    public Response getChallenge() {
+        return challenge;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
index 4d441cd..f6b1e62 100755
--- a/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
+++ b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java
@@ -28,18 +28,14 @@ public interface RequiredActionProvider extends Provider {
      * @param context
      * @return
      */
-    Response requiredActionChallenge(RequiredActionContext context);
+    void requiredActionChallenge(RequiredActionContext context);
 
     /**
-     * This is an optional method.  If the required action has a more complex interaction, you can encapsulate it within
-     * a REST service.  This method returns a JAX-RS sub locator object that can be referenced at:
-     *
-     * /realms/{realm}/login-actions/required-actions/{provider-id}
+     * Called when a required action has form input you want to process.
      *
      * @param context
-     * @return
      */
-    Object jaxrsService(RequiredActionContext context);
+    void processAction(RequiredActionContext context);
 
     /**
      * Provider id of this required action.  Must match ProviderFactory.getId().
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 6f094f7..627c80f 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -4,56 +4,20 @@ import org.keycloak.Config;
 import org.keycloak.authentication.RequiredActionContext;
 import org.keycloak.authentication.RequiredActionFactory;
 import org.keycloak.authentication.RequiredActionProvider;
-import org.keycloak.events.Errors;
-import org.keycloak.freemarker.FreeMarkerException;
-import org.keycloak.login.LoginFormsProvider;
+import org.keycloak.authentication.AbstractFormRequiredAction;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.protocol.LoginProtocol;
-import org.keycloak.services.managers.AuthenticationManager;
 
-import javax.ws.rs.Consumes;
-import javax.ws.rs.POST;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.HashMap;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
+public class TermsAndConditions extends AbstractFormRequiredAction implements RequiredActionFactory {
 
     public static final String PROVIDER_ID = "terms_and_conditions";
 
-    public static class Resource {
-
-        public Resource(RequiredActionContext context) {
-            this.context = context;
-        }
-
-        protected RequiredActionContext context;
-
-        @POST
-        @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-        public Response agree(final MultivaluedMap<String, String> formData)  throws URISyntaxException, IOException, FreeMarkerException {
-            if (formData.containsKey("cancel")) {
-                LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
-                protocol.setRealm(context.getRealm())
-                        .setHttpHeaders(context.getHttpRequest().getHttpHeaders())
-                        .setUriInfo(context.getUriInfo());
-                context.getEvent().error(Errors.REJECTED_BY_USER);
-                return protocol.consentDenied(context.getClientSession());
-            }
-            context.getUser().removeRequiredAction(PROVIDER_ID);
-            return AuthenticationManager.nextActionAfterAuthentication(context.getSession(), context.getUserSession(), context.getClientSession(), context.getConnection(), context.getHttpRequest(), context.getUriInfo(), context.getEvent());
-        }
-
-    }
-
     @Override
     public RequiredActionProvider create(KeycloakSession session) {
         return this;
@@ -87,17 +51,21 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
 
     }
 
+
     @Override
-    public Response requiredActionChallenge(RequiredActionContext context) {
-        return context.getSession().getProvider(LoginFormsProvider.class)
-                .setClientSessionCode(context.generateAccessCode(getProviderId()))
-                .setUser(context.getUser())
-                .createForm("terms.ftl", new HashMap<String, Object>());
+    public void requiredActionChallenge(RequiredActionContext context) {
+        Response challenge =  form(context).createForm("terms.ftl");
+        context.challenge(challenge);
     }
 
     @Override
-    public Object jaxrsService(RequiredActionContext context) {
-        return new Resource(context);
+    public void processAction(RequiredActionContext context) {
+        if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) {
+            context.failure();
+            return;
+        }
+        context.success();
+
     }
 
     @Override
@@ -105,8 +73,4 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
         return "Terms and Conditions";
     }
 
-    @Override
-    public void close() {
-
-    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
index b00510c..4ef413d 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -48,21 +48,20 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
     }
 
     @Override
-    public Response requiredActionChallenge(RequiredActionContext context) {
+    public void requiredActionChallenge(RequiredActionContext context) {
         LoginFormsProvider loginFormsProvider = context.getSession()
                 .getProvider(LoginFormsProvider.class)
                 .setClientSessionCode(context.generateAccessCode(getProviderId()))
                 .setUser(context.getUser());
-        return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
+        Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
+        context.challenge(challenge);
     }
 
     @Override
-    public Object jaxrsService(RequiredActionContext context) {
-        // this is handled by LoginActionsService at the moment
-        return null;
+    public void processAction(RequiredActionContext context) {
+        context.failure();
     }
 
-
     @Override
     public void close() {
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
index 5860928..4cb8493 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java
@@ -23,18 +23,17 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
     }
 
     @Override
-    public Response requiredActionChallenge(RequiredActionContext context) {
+    public void requiredActionChallenge(RequiredActionContext context) {
         LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
                 .setClientSessionCode(context.generateAccessCode(getProviderId()))
                 .setUser(context.getUser());
-        return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
+        Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
+        context.challenge(challenge);
     }
 
     @Override
-    public Object jaxrsService(RequiredActionContext context) {
-        // this is handled by LoginActionsService at the moment
-        // todo should be refactored to contain it here
-        return null;
+    public void processAction(RequiredActionContext context) {
+        context.failure();
     }
 
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
index ac4e187..c958357 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java
@@ -25,18 +25,17 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
     }
 
     @Override
-    public Response requiredActionChallenge(RequiredActionContext context) {
+    public void requiredActionChallenge(RequiredActionContext context) {
          LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
                  .setClientSessionCode(context.generateAccessCode(getProviderId()))
                 .setUser(context.getUser());
-        return loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
+        Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
+        context.challenge(challenge);
     }
 
     @Override
-    public Object jaxrsService(RequiredActionContext context) {
-        // this is handled by LoginActionsService at the moment
-        // todo should be refactored to contain it here
-        return null;
+    public void processAction(RequiredActionContext context) {
+        context.failure();
     }
 
 
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index f8a6d96..ba0ee06 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -34,9 +34,10 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
         }
     }
     @Override
-    public Response requiredActionChallenge(RequiredActionContext context) {
+    public void requiredActionChallenge(RequiredActionContext context) {
         if (Validation.isBlank(context.getUser().getEmail())) {
-            return null;
+            context.ignore();
+            return;
         }
 
         context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
@@ -45,13 +46,13 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
         LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
                 .setClientSessionCode(context.generateAccessCode(getProviderId()))
                 .setUser(context.getUser());
-        return loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
+        Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
+        context.challenge(challenge);
     }
 
     @Override
-    public Object jaxrsService(RequiredActionContext context) {
-        // this is handled by LoginActionsService at the moment
-        return null;
+    public void processAction(RequiredActionContext context) {
+        context.failure();
     }
 
 
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 d3ab117..9e5cd97 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -7,10 +7,13 @@ import org.keycloak.ClientConnection;
 import org.keycloak.RSATokenVerifier;
 import org.keycloak.VerificationException;
 import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionContextResult;
 import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
 import org.keycloak.jose.jws.JWSBuilder;
 import org.keycloak.login.LoginFormsProvider;
 import org.keycloak.models.ClientModel;
@@ -418,8 +421,9 @@ public class AuthenticationManager {
         final UserModel user = userSession.getUser();
         final ClientModel client = clientSession.getClient();
 
-        RequiredActionContext context = evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
+        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());
 
@@ -429,11 +433,23 @@ public class AuthenticationManager {
         for (String action : requiredActions) {
             RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
             RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
-            Response challenge = actionProvider.requiredActionChallenge(context);
-            if (challenge != null) {
-                return challenge;
+            actionProvider.requiredActionChallenge(context);
+
+            if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
+                LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
+                protocol.setRealm(context.getRealm())
+                        .setHttpHeaders(context.getHttpRequest().getHttpHeaders())
+                        .setUriInfo(context.getUriInfo());
+                event.error(Errors.REJECTED_BY_USER);
+                return protocol.consentDenied(context.getClientSession());
+            }
+            else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
+                return context.getChallenge();
+            }
+            else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
+                event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, actionProvider.getProviderId()).success();
+                clientSession.getUserSession().getUser().removeRequiredAction(actionProvider.getProviderId());
             }
-
         }
         if (client.isConsentRequired()) {
 
@@ -484,58 +500,26 @@ public class AuthenticationManager {
 
     }
 
-    public static RequiredActionContext 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) {
-        RequiredActionContext context = new RequiredActionContext() {
-            @Override
-            public EventBuilder getEvent() {
-                return event;
-            }
-
-            @Override
-            public UserModel getUser() {
-                return user;
-            }
-
-            @Override
-            public RealmModel getRealm() {
-                return realm;
-            }
-
-            @Override
-            public ClientSessionModel getClientSession() {
-                return clientSession;
-            }
-
-            @Override
-            public UserSessionModel getUserSession() {
-                return userSession;
-            }
-
-            @Override
-            public ClientConnection getConnection() {
-                return clientConnection;
-            }
-
+    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 UriInfo getUriInfo() {
-                return uriInfo;
+            public void challenge(Response response) {
+                throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
             }
 
             @Override
-            public KeycloakSession getSession() {
-                return session;
+            public void failure() {
+                throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
             }
 
             @Override
-            public HttpRequest getHttpRequest() {
-                return request;
+            public void success() {
+                throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
             }
 
             @Override
-            public String generateAccessCode(String action) {
-                ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
-                code.setAction(action);
-                return code.getCode();
+            public void ignore() {
+                throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
             }
         };
 
@@ -543,9 +527,8 @@ public class AuthenticationManager {
         for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
             if (!model.isEnabled()) continue;
             RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
-            provider.evaluateTriggers(context);
+            provider.evaluateTriggers(result);
         }
-        return context;
     }
 
 
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 dc83039..e178939 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -26,6 +26,7 @@ import org.jboss.resteasy.spi.HttpRequest;
 import org.keycloak.ClientConnection;
 import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionContextResult;
 import org.keycloak.authentication.RequiredActionProvider;
 import org.keycloak.email.EmailException;
 import org.keycloak.email.EmailProvider;
@@ -125,6 +126,10 @@ public class LoginActionsService {
         return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "authenticateForm");
     }
 
+    public static UriBuilder requiredActionProcessor(UriInfo uriInfo) {
+        return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "requiredActionPOST");
+    }
+
     public static UriBuilder registrationFormProcessor(UriInfo uriInfo) {
         return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
     }
@@ -829,10 +834,26 @@ public class LoginActionsService {
         }
     }
 
-    @Path("required-actions/{action}")
-    public Object requiredAction(@QueryParam("code") final String code,
-                                 @PathParam("action") String action) {
-        event.event(EventType.LOGIN);
+    @Path("required-action")
+    @POST
+    public Response requiredActionPOST(@QueryParam("code") final String code,
+                                       @QueryParam("action") String action) {
+        return processRequireAction(code, action);
+
+
+
+    }
+
+    @Path("required-action")
+    @GET
+    public Response requiredActionGET(@QueryParam("code") final String code,
+                                       @QueryParam("action") String action) {
+        return processRequireAction(code, action);
+    }
+
+    public Response processRequireAction(final String code, String action) {
+        event.event(EventType.CUSTOM_REQUIRED_ACTION);
+        event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
         if (action == null) {
             logger.error("required action query param was null");
             event.error(Errors.INVALID_CODE);
@@ -859,54 +880,11 @@ public class LoginActionsService {
             throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
         }
 
+        initEvent(clientSession);
 
-        RequiredActionContext context = new RequiredActionContext() {
-            @Override
-            public EventBuilder getEvent() {
-                return event;
-            }
-
-            @Override
-            public UserModel getUser() {
-                return getUserSession().getUser();
-            }
-
-            @Override
-            public RealmModel getRealm() {
-                return realm;
-            }
-
-            @Override
-            public ClientSessionModel getClientSession() {
-                return clientSession;
-            }
-
-            @Override
-            public UserSessionModel getUserSession() {
-                return clientSession.getUserSession();
-            }
-
-            @Override
-            public ClientConnection getConnection() {
-                return clientConnection;
-            }
-
-            @Override
-            public UriInfo getUriInfo() {
-                return uriInfo;
-            }
-
-            @Override
-            public KeycloakSession getSession() {
-                return session;
-            }
-
-            @Override
-            public HttpRequest getHttpRequest() {
-                return request;
-            }
 
-            @Override
+        RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser()) {
+             @Override
             public String generateAccessCode(String action) {
                 String clientSessionAction = clientSession.getAction();
                 if (action.equals(clientSessionAction)) {
@@ -917,10 +895,32 @@ public class LoginActionsService {
                 code.setAction(action);
                 return code.getCode();
             }
-        };
-        return provider.jaxrsService(context);
-
 
+            @Override
+            public void ignore() {
+                throw new RuntimeException("Cannot call ignore within processAction()");
+            }
+        };
+        provider.processAction(context);
+        if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
+            event.clone().event(EventType.CUSTOM_REQUIRED_ACTION)
+                         .detail(Details.CUSTOM_REQUIRED_ACTION, action).success();
+            clientSession.getUserSession().getUser().removeRequiredAction(provider.getProviderId());
+            return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
+        }
+        if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
+            return context.getChallenge();
+        }
+        if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
+            LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
+            protocol.setRealm(context.getRealm())
+                    .setHttpHeaders(context.getHttpRequest().getHttpHeaders())
+                    .setUriInfo(context.getUriInfo());
+            event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER);
+            return protocol.consentDenied(context.getClientSession());
+        }
+
+        throw new RuntimeException("Unreachable");
     }
 
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
new file mode 100755
index 0000000..a1636c8
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
@@ -0,0 +1,119 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.actions;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.authentication.requiredactions.TermsAndConditions;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventType;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
+import org.keycloak.testsuite.pages.TermsAndConditionsPage;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class TermsAndConditionsTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule();
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected AppPage appPage;
+
+    @WebResource
+    protected LoginPage loginPage;
+
+    @WebResource
+    protected TermsAndConditionsPage termsPage;
+
+    @Before
+    public void before() {
+        keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+            @Override
+            public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
+                UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+                user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
+            }
+        });
+    }
+
+    @Test
+    public void termsAccepted() {
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+
+        termsPage.assertCurrent();
+
+        termsPage.acceptTerms();
+
+        String sessionId = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent().getSessionId();
+
+        Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+        events.expectLogin().session(sessionId).assertEvent();
+    }
+
+    @Test
+    public void termsDeclined() {
+        loginPage.open();
+
+        loginPage.login("test-user@localhost", "password");
+
+        termsPage.assertCurrent();
+
+        termsPage.declineTerms();
+
+        events.expectLogin().detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID)
+                .error(Errors.REJECTED_BY_USER)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+
+    }
+
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/TermsAndConditionsPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/TermsAndConditionsPage.java
new file mode 100755
index 0000000..478716f
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/TermsAndConditionsPage.java
@@ -0,0 +1,54 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.pages;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class TermsAndConditionsPage extends AbstractPage {
+
+    @FindBy(id = "kc-accept")
+    private WebElement submitButton;
+
+    @FindBy(id = "kc-decline")
+    private WebElement cancelButton;
+
+    public boolean isCurrent() {
+        return driver.getTitle().equals("Terms and Conditions");
+    }
+
+    public void acceptTerms() {
+        submitButton.click();
+    }
+    public void declineTerms() {
+        cancelButton.click();
+    }
+
+    @Override
+    public void open() {
+        throw new UnsupportedOperationException();
+    }
+
+}