keycloak-aplcache

doco

8/12/2015 9:37:51 PM

Details

diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 29a49a7..7a798f8 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -46,6 +46,7 @@
                 <!ENTITY CustomAttributes SYSTEM "modules/custom-attributes.xml">
                 <!ENTITY ProtocolMappers SYSTEM "modules/protocol-mappers.xml">
                 <!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
+                <!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
                 ]>
 
 <book>
@@ -140,6 +141,7 @@ This one is short
     &Proxy;
     &CustomAttributes;
     &ProtocolMappers;
+    &AuthSPI;
     &Migration;
 
 </book>
diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index 8e1ac29..e849273 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -268,7 +268,96 @@ Forms Subflow - ALTERNATIVE
             </para>
             <para>
                 Now we are getting into the meat of the Authenticator implementation.  The next method to implement is
-                authenticate().  This is the initial method the flow invokes when the execution is first visited.
+                authenticate().  This is the initial method the flow invokes when the execution is first visited.  What we
+                want is that if a user has answered the secret question already on their browser's machine, that the
+                user doesn't have to answer the question again.  Basically making that machine "trusted".  The authenticate()
+                method isn't responsible for processing the secret question form.  Its sole purpose is to render the page
+                or to continue the flow.
+<programlisting>
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        if (hasCookie(context)) {
+           context.success();
+           return;
+        }
+        Response challenge = loginForm(context).createForm("secret_question.ftl");
+        context.challenge(challenge);
+    }
+</programlisting>
+            </para>
+            <para>
+                If the hasCookie() method checks to see if there is already a cookie set on the browser which indicates
+                that the secret question has already been answered.  If that returns true, we just mark this execution's
+                status as SUCCESS using the AuthenticationFlowContext.success() method and returning from the authentication()
+                method.
+            </para>
+            <para>
+                If the hasCookie() method returns false, we must return a response that renders the secret question HTML
+                form.  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
+                to build the form.  This page builder is called <literal>org.keycloak.login.LoginFormsProvider</literal>.
+                the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme.  Additionally
+                you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the
+                Freemarker template.  We'll go over this later.
+            </para>
+            <para>
+                Calling LoginFormsProvider.createForm() returns a JAX-RS Response object.  We then call AuthenticationFlowContext.challenge()
+                passing in this response.  This sets the status of the execution as CHALLENGE and if the execution is Required, this
+                JAX-RS Response object will be sent to the browser.
+            </para>
+            <para>
+                So, the HTML page asking for the answer to a secret question is displayed to the user and the user
+                enteres in the answer and clicks submit.  The action URL of the HTML form will send an HTTP request to the
+                flow.  The flow will end up invoking the action() method of our Authenticator implementation.
+<programlisting>
+    @Override
+    public void action(AuthenticationFlowContext context) {
+        boolean validated = validateAnswer(context);
+        if (!validated) {
+           Response challenge = loginForm(context)
+                                 .setError("badSecret")
+                                 .createForm("secret_question.ftl");
+           context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
+           return;
+        }
+        setCookie(context);
+        context.success();
+    }
+
+
+</programlisting>
+            </para>
+            <para>
+                If the answer is not valid, we rebuild the HTML Form with an additional error message.  We then call
+                AuthenticationFlowContext.failureChallenge() passing in the reason for the value and the JAX-RS response.
+                failureChallenge() works the same as challenge(), but it also records the failure so it can be analyzed
+                by any attack detection service.
+            </para>
+            <para>
+                If validation is successful, then we set a cookie to remember that the secret question has been answered
+                and we call AuthenticationFlowContext.success().
+            </para>
+            <para>
+                The last thing I want to go over is the setCookie() method.  This is an example of providing configuration
+                for the Authenticator.  In this case we want the max age of the cookie to be configurable.
+<programlisting>
+    protected void setCookie(AuthenticationFlowContext context) {
+        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
+        int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
+        if (config != null) {
+            maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
+
+        }
+        ... set the cookie ...
+    }
+
+</programlisting>
+            </para>
+            <para>
+                We obtain an AuthenticatorConfigModel from the AuthenticationFlowContext.getAuthenticatorConfig() method.
+                If configuration exists we pull the max age config out of it.  We will see how we can define what should
+                be configured when we talk about the AuthenticatorFactory implementation.  The config values can be
+                defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
             </para>
         </section>
     </section>
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
index 8e60d9b..2814a4d 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
@@ -3,12 +3,15 @@ package org.keycloak.examples.authenticator;
 import org.keycloak.authentication.AuthenticationFlowContext;
 import org.keycloak.authentication.AuthenticationFlowError;
 import org.keycloak.authentication.AbstractFormAuthenticator;
+import org.keycloak.models.AuthenticatorConfigModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.utils.CredentialValidation;
+import org.keycloak.services.util.CookieHelper;
 
+import javax.ws.rs.core.Cookie;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
@@ -20,21 +23,52 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
 
     public static final String CREDENTIAL_TYPE = "secret_question";
 
+    protected boolean hasCookie(AuthenticationFlowContext context) {
+        Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
+        return cookie != null;
+    }
+
     @Override
     public void authenticate(AuthenticationFlowContext context) {
+        if (hasCookie(context)) {
+            context.success();
+            return;
+        }
         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);
+        boolean validated = validateAnswer(context);
+        if (!validated) {
+            Response challenge =  loginForm(context)
+                    .setError("badSecret")
+                    .createForm("secret_question.ftl");
+            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
             return;
         }
+        setCookie(context);
+        context.success();
+    }
+
+    protected void setCookie(AuthenticationFlowContext context) {
+        AuthenticatorConfigModel config = context.getAuthenticatorConfig();
+        int maxCookieAge = 60 * 60 * 24 * 30; // 30 days
+        if (config != null) {
+            maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
+
+        }
+        CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
+                context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(),
+                null, null,
+                maxCookieAge,
+                true, true);
+    }
 
+    protected boolean validateAnswer(AuthenticationFlowContext context) {
+        MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
+        String secret = formData.getFirst("secret");
         UserCredentialValueModel cred = null;
         for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
             if (model.getType().equals(CREDENTIAL_TYPE)) {
@@ -42,25 +76,8 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
                 break;
             }
         }
-        if (cred == null) {
-            badSecret(context);
-            return;
-        }
-
-        boolean validated = CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
-        if (!validated) {
-            badSecret(context);
-            return;
-        }
-
-        context.success();
-    }
 
-    private void badSecret(AuthenticationFlowContext context) {
-        Response challenge =  loginForm(context)
-                .setError("badSecret")
-                .createForm("secret_question.ftl");
-        context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
+        return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
     }
 
     @Override