diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml
index e849273876..05e7f4235e 100755
--- a/docbook/reference/en/en-US/modules/auth-spi.xml
+++ b/docbook/reference/en/en-US/modules/auth-spi.xml
@@ -226,6 +226,9 @@ Forms Subflow - ALTERNATIVE
This services/ file is used by Keycloak to scan the providers it has to load into the system.
+
+ To deploy this jar, just copy it to the standalone/configuration/providers directory.
+ Implementing an Authenticator
@@ -293,8 +296,7 @@ Forms Subflow - ALTERNATIVE
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 org.keycloak.authentication.AbstractFormAuthenticator
- it has a loginForm() method that initializes a Freemarker page builder with appropriate base information needed
+ form. AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed
to build the form. This page builder is called org.keycloak.login.LoginFormsProvider.
the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme. Additionally
you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the
@@ -314,9 +316,9 @@ Forms Subflow - ALTERNATIVE
public void action(AuthenticationFlowContext context) {
boolean validated = validateAnswer(context);
if (!validated) {
- Response challenge = loginForm(context)
+ Response challenge = context.form()
.setError("badSecret")
- .createForm("secret_question.ftl");
+ .createForm("secret-question.ftl");
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return;
}
@@ -360,5 +362,165 @@ Forms Subflow - ALTERNATIVE
defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
+
+ Implementing an AuthenticatorFactory
+
+ The next step in this process is to implement an AuthenticatorFactory. This factory is responsible
+ for instantiating an Authenticator. It also provides deployment and configuration metadata about
+ the Authenticator.
+
+
+ The getId() method is just the unique name of the component. The create() methods should also
+ be self explanatory. The create(KeycloakSession) method will actually never be called. It is just
+ an artifact of the more generic ProviderFactory interface.
+
+public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "secret-question-authenticator";
+ private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public Authenticator create() {
+ return SINGLETON;
+ }
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+
+
+
+ The next thing the factory is responsible for is specify the allowed requirement switches. While there
+ are four different requirement types: ALTERNATIVE, REQUIRED, OPTIONAL, DISABLED, AuthenticatorFactory
+ implementations can limit which requirement options are shown in the admin console when defining
+ a flow. In our example, we're going to limit our requirement options to REQUIRED and DISABLED.
+
+ private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+ AuthenticationExecutionModel.Requirement.REQUIRED,
+ AuthenticationExecutionModel.Requirement.DISABLED
+ };
+ @Override
+ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+ return REQUIREMENT_CHOICES;
+ }
+
+
+
+ The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not
+ Authenticator.setRequiredActions() method will be called. If an Authenticator is not configured for a user,
+ the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error. If it
+ returns true, then the flow manager will invoke Authenticator.setRequiredActions().
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return true;
+ }
+
+
+
+ The next few methods define how the Authenticator can be configured. The isConfigurable() method
+ is a flag which specifies to the admin console on whether the Authenticator can be configured within
+ a flow. The getConfigProperties() method returns a list of ProviderConfigProperty objects. These
+ objects define a specific configuration attribute.
+ getConfigProperties() {
+ return configProperties;
+ }
+
+ private static final List configProperties = new ArrayList();
+
+ static {
+ ProviderConfigProperty property;
+ property = new ProviderConfigProperty();
+ property.setName("cookie.max.age");
+ property.setLabel("Cookie Max Age");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
+ configProperties.add(property);
+ }
+]]>
+
+
+ Each ProviderConfigProperty defines the name of the config property. This is the key used in the config
+ map stored in AuthenticatorConfigModel. The label defines how the config option will be displayed in the
+ admin console. The type defines if it is a String, Boolean, or other type. The admin console
+ will display different UI inputs depending on the type. The help text is what will be shown in the
+ tooltip for the config attribute in the admin console. Read the javadoc of ProviderConfigProperty
+ for more detail.
+
+
+ The rest of the methods are for the admin console. getHelpText() is the tooltip text that will be
+ shown when you are picking the Authenticator you want to bind to an execution. getDisplayType()
+ is what text that will be shown in the admin console when listing the Authenticator. getReferenceCategory()
+ is just a category the Authenticator belongs to.
+
+
+
+ Adding Authenticator Form
+
+ Keycloak comes with a Freemarker theme and template engine. The createForm()
+ method you called within authenticate() of your Authenticator class, builds an HTML page from a file within
+ your login them: secret-question.ftl. This file should be placed in the login theme with all the other
+ .ftl files you see for login.
+
+
+ Let's take a bigger look at secret-question.ftl Here's a small code snippet:
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+ Any piece of text enclosed in ${} corresponds to an attribute or template funtion.
+ If you see the form's action, you see it points to ${url.loginAction}. This value
+ is automatically generated when you invoke the AuthenticationFlowContext.form() method. You can also obtain
+ this value by calling the AuthenticationFlowContext.getActionURL() method in Java code.
+
+
+ You'll also see ${properties.someValue}. These correspond to properties defined
+ in your theme.properties file of our theme. ${msg("someValue")} corresponds to the
+ internationalized message bundles (.properties files) included with the login theme messages/ directory.
+ If you're just using english, you can just add the value of the loginSecretQuestion
+ value. This should be the question you want to ask the user.
+
+
+ When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance. If you called,
+ LoginFormsProvider.setAttribute("foo", "bar"), the value of "foo" would be available
+ for reference in your form as ${foo}. The value of an attribute can be any Java
+ bean as well.
+
+
+
+ Adding Authenticator to a Flow
+
+ Adding an Authenticator to a flow must be done in the admin console.
+ If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
+ defined flows. You cannot modify an built in flows, so, to add the Authenticator we've created you
+ have to copy an existing flow or create your own. I'm hoping the UI is intuitive enough so that you
+ can figure out for yourself how to create a flow and add the Authenticator.
+
+
+ After you've created your flow, you have to bind it to the login action you want to bind it to. If you go
+ to the Authentication menu and go to the Bindings tab you will see options to bind a flow to
+ the browser, registration, or direct grant flow.
+
+
\ No newline at end of file
diff --git a/examples/providers/authenticator/secret-question.ftl b/examples/providers/authenticator/secret-question.ftl
new file mode 100755
index 0000000000..b69c5206a2
--- /dev/null
+++ b/examples/providers/authenticator/secret-question.ftl
@@ -0,0 +1,34 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "title">
+ ${msg("loginTitle",realm.name)}
+ <#elseif section = "header">
+ ${msg("loginTitleHtml",realm.name)}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
index 2814a4d55f..1f4a8aaa00 100755
--- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
@@ -2,13 +2,12 @@ package org.keycloak.examples.authenticator;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.AbstractFormAuthenticator;
+import org.keycloak.authentication.Authenticator;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.services.util.CookieHelper;
import javax.ws.rs.core.Cookie;
@@ -19,7 +18,7 @@ import javax.ws.rs.core.Response;
* @author Bill Burke
* @version $Revision: 1 $
*/
-public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
+public class SecretQuestionAuthenticator implements Authenticator {
public static final String CREDENTIAL_TYPE = "secret_question";
@@ -34,7 +33,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
context.success();
return;
}
- Response challenge = loginForm(context).createForm("secret_question.ftl");
+ Response challenge = context.form().createForm("secret_question.ftl");
context.challenge(challenge);
}
@@ -42,9 +41,9 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
public void action(AuthenticationFlowContext context) {
boolean validated = validateAnswer(context);
if (!validated) {
- Response challenge = loginForm(context)
+ Response challenge = context.form()
.setError("badSecret")
- .createForm("secret_question.ftl");
+ .createForm("secret-question.ftl");
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return;
}
@@ -68,7 +67,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
- String secret = formData.getFirst("secret");
+ String secret = formData.getFirst("secret_answer");
UserCredentialValueModel cred = null;
for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
if (model.getType().equals(CREDENTIAL_TYPE)) {
@@ -77,7 +76,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
}
}
- return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
+ return cred.getValue().equals(secret);
}
@Override
@@ -92,6 +91,11 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
- user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG");
+ user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
+ }
+
+ @Override
+ public void close() {
+
}
}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
index 09a83da758..09be4c1747 100755
--- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
@@ -21,6 +21,11 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
public static final String PROVIDER_ID = "secret-question-authenticator";
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
@Override
public Authenticator create() {
return SINGLETON;
@@ -31,21 +36,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
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
@@ -61,8 +51,13 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
}
@Override
- public String getHelpText() {
- return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
+ public boolean isConfigurable() {
+ return true;
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
}
private static final List configProperties = new ArrayList();
@@ -70,17 +65,27 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
- property.setName("remember_machine");
- property.setLabel("Remember machine");
- property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
- property.setHelpText("If set to true, a checkbox will appear when entering in secret on whether the user wants keycloak to remember the machine. If the user wants to remember, then a persistent cookie is set, and the user will not have to enter in their secret again.");
+ property.setName("cookie.max.age");
+ property.setLabel("Cookie Max Age");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
configProperties.add(property);
}
@Override
- public List getConfigProperties() {
- return configProperties;
+ public String getHelpText() {
+ return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Secret Question";
+ }
+
+ @Override
+ public String getReferenceCategory() {
+ return "Secret Question";
}
@Override
@@ -98,8 +103,5 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
}
- @Override
- public String getId() {
- return PROVIDER_ID;
- }
+
}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java
new file mode 100755
index 0000000000..6644b8e2df
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java
@@ -0,0 +1,47 @@
+package org.keycloak.examples.authenticator;
+
+import org.keycloak.authentication.RequiredActionContext;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.models.UserCredentialValueModel;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class SecretQuestionRequiredAction implements RequiredActionProvider {
+ public static final String PROVIDER_ID = "secret_question_config";
+
+ @Override
+ public void evaluateTriggers(RequiredActionContext context) {
+
+ }
+
+ @Override
+ public void requiredActionChallenge(RequiredActionContext context) {
+ Response challenge = context.form().createForm("secret_question_config.ftl");
+ context.challenge(challenge);
+
+ }
+
+ @Override
+ public void processAction(RequiredActionContext context) {
+ String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer"));
+ UserCredentialValueModel model = new UserCredentialValueModel();
+ model.setValue(answer);
+ model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
+ context.getUser().updateCredentialDirectly(model);
+ context.success();
+ }
+
+ @Override
+ public String getProviderId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java
new file mode 100755
index 0000000000..ff699e27ef
--- /dev/null
+++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java
@@ -0,0 +1,48 @@
+package org.keycloak.examples.authenticator;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.RequiredActionFactory;
+import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author Bill Burke
+ * @version $Revision: 1 $
+ */
+public class SecretQuestionRequiredActionFactory implements RequiredActionFactory {
+
+ private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction();
+
+ @Override
+ public RequiredActionProvider create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+
+ @Override
+ public String getId() {
+ return SecretQuestionRequiredAction.PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayText() {
+ return "Secret Question";
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
index b085eeb540..e472fff1ff 100755
--- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl
@@ -6,9 +6,6 @@
${msg("loginTitleHtml",realm.name)}
<#elseif section = "form">