auth spi refactor and doco

This commit is contained in:
Bill Burke 2015-08-13 11:28:11 -04:00
parent 523bdd5ec9
commit 8a23463328
20 changed files with 520 additions and 204 deletions

View file

@ -226,6 +226,9 @@ Forms Subflow - ALTERNATIVE
<para> <para>
This services/ file is used by Keycloak to scan the providers it has to load into the system. This services/ file is used by Keycloak to scan the providers it has to load into the system.
</para> </para>
<para>
To deploy this jar, just copy it to the standalone/configuration/providers directory.
</para>
</section> </section>
<section> <section>
<title>Implementing an Authenticator</title> <title>Implementing an Authenticator</title>
@ -293,8 +296,7 @@ Forms Subflow - ALTERNATIVE
</para> </para>
<para> <para>
If the hasCookie() method returns false, we must return a response that renders the secret question HTML 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> form. AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed
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>. 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 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 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) { public void action(AuthenticationFlowContext context) {
boolean validated = validateAnswer(context); boolean validated = validateAnswer(context);
if (!validated) { if (!validated) {
Response challenge = loginForm(context) Response challenge = context.form()
.setError("badSecret") .setError("badSecret")
.createForm("secret_question.ftl"); .createForm("secret-question.ftl");
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return; return;
} }
@ -360,5 +362,165 @@ Forms Subflow - ALTERNATIVE
defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation. defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
</para> </para>
</section> </section>
<section>
<title>Implementing an AuthenticatorFactory</title>
<para>
The next step in this process is to implement an AuthenticatorFactory. This factory is responsible
for instantiating an Authenticator. It also provides deployment and configuration metadata about
the Authenticator.
</para>
<para>
The getId() method is just the unique name of the component. The create() methods should also
be self explanatory. The create(KeycloakSession) method will actually never be called. It is just
an artifact of the more generic ProviderFactory interface.
<programlisting>
public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
public static final String PROVIDER_ID = "secret-question-authenticator";
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public Authenticator create() {
return SINGLETON;
}
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
</programlisting>
</para>
<para>
The next thing the factory is responsible for is specify the allowed requirement switches. While there
are four different requirement types: ALTERNATIVE, REQUIRED, OPTIONAL, DISABLED, AuthenticatorFactory
implementations can limit which requirement options are shown in the admin console when defining
a flow. In our example, we're going to limit our requirement options to REQUIRED and DISABLED.
<programlisting>
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
</programlisting>
</para>
<para>
The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not
Authenticator.setRequiredActions() method will be called. If an Authenticator is not configured for a user,
the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error. If it
returns true, then the flow manager will invoke Authenticator.setRequiredActions().
<programlisting>
@Override
public boolean isUserSetupAllowed() {
return true;
}
</programlisting>
</para>
<para>
The next few methods define how the Authenticator can be configured. The isConfigurable() method
is a flag which specifies to the admin console on whether the Authenticator can be configured within
a flow. The getConfigProperties() method returns a list of ProviderConfigProperty objects. These
objects define a specific configuration attribute.
<programlisting><![CDATA[
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName("cookie.max.age");
property.setLabel("Cookie Max Age");
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
configProperties.add(property);
}
]]></programlisting>
</para>
<para>
Each ProviderConfigProperty defines the name of the config property. This is the key used in the config
map stored in AuthenticatorConfigModel. The label defines how the config option will be displayed in the
admin console. The type defines if it is a String, Boolean, or other type. The admin console
will display different UI inputs depending on the type. The help text is what will be shown in the
tooltip for the config attribute in the admin console. Read the javadoc of ProviderConfigProperty
for more detail.
</para>
<para>
The rest of the methods are for the admin console. getHelpText() is the tooltip text that will be
shown when you are picking the Authenticator you want to bind to an execution. getDisplayType()
is what text that will be shown in the admin console when listing the Authenticator. getReferenceCategory()
is just a category the Authenticator belongs to.
</para>
</section>
<section>
<title>Adding Authenticator Form</title>
<para>
Keycloak comes with a Freemarker <link linkend="themes">theme and template engine</link>. The createForm()
method you called within authenticate() of your Authenticator class, builds an HTML page from a file within
your login them: secret-question.ftl. This file should be placed in the login theme with all the other
.ftl files you see for login.
</para>
<para>
Let's take a bigger look at secret-question.ftl Here's a small code snippet:
<programlisting><![CDATA[
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginSecretQuestion")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
</div>
</div>
]]></programlisting>
</para>
<para>
Any piece of text enclosed in <literal>${}</literal> corresponds to an attribute or template funtion.
If you see the form's action, you see it points to <literal>${url.loginAction}</literal>. This value
is automatically generated when you invoke the AuthenticationFlowContext.form() method. You can also obtain
this value by calling the AuthenticationFlowContext.getActionURL() method in Java code.
</para>
<para>
You'll also see <literal>${properties.someValue}</literal>. These correspond to properties defined
in your theme.properties file of our theme. <literal>${msg("someValue")}</literal> corresponds to the
internationalized message bundles (.properties files) included with the login theme messages/ directory.
If you're just using english, you can just add the value of the <literal>loginSecretQuestion</literal>
value. This should be the question you want to ask the user.
</para>
<para>
When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance. If you called,
<literal>LoginFormsProvider.setAttribute("foo", "bar")</literal>, the value of "foo" would be available
for reference in your form as <literal>${foo}</literal>. The value of an attribute can be any Java
bean as well.
</para>
</section>
<section>
<title>Adding Authenticator to a Flow</title>
<para>
Adding an Authenticator to a flow must be done in the admin console.
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
defined flows. You cannot modify an built in flows, so, to add the Authenticator we've created you
have to copy an existing flow or create your own. I'm hoping the UI is intuitive enough so that you
can figure out for yourself how to create a flow and add the Authenticator.
</para>
<para>
After you've created your flow, you have to bind it to the login action you want to bind it to. If you go
to the Authentication menu and go to the Bindings tab you will see options to bind a flow to
the browser, registration, or direct grant flow.
</para>
</section>
</section> </section>
</chapter> </chapter>

View file

@ -0,0 +1,34 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<#if section = "title">
${msg("loginTitle",realm.name)}
<#elseif section = "header">
${msg("loginTitleHtml",realm.name)}
<#elseif section = "form">
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="totp" class="${properties.kcLabelClass!}">What is your mom's first name</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<div class="${properties.kcFormButtonsWrapperClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
</div>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -2,13 +2,12 @@ package org.keycloak.examples.authenticator;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AbstractFormAuthenticator; import org.keycloak.authentication.Authenticator;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.CookieHelper;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
@ -19,7 +18,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { public class SecretQuestionAuthenticator implements Authenticator {
public static final String CREDENTIAL_TYPE = "secret_question"; public static final String CREDENTIAL_TYPE = "secret_question";
@ -34,7 +33,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
context.success(); context.success();
return; return;
} }
Response challenge = loginForm(context).createForm("secret_question.ftl"); Response challenge = context.form().createForm("secret_question.ftl");
context.challenge(challenge); context.challenge(challenge);
} }
@ -42,9 +41,9 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
boolean validated = validateAnswer(context); boolean validated = validateAnswer(context);
if (!validated) { if (!validated) {
Response challenge = loginForm(context) Response challenge = context.form()
.setError("badSecret") .setError("badSecret")
.createForm("secret_question.ftl"); .createForm("secret-question.ftl");
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
return; return;
} }
@ -68,7 +67,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
protected boolean validateAnswer(AuthenticationFlowContext context) { protected boolean validateAnswer(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String secret = formData.getFirst("secret"); String secret = formData.getFirst("secret_answer");
UserCredentialValueModel cred = null; UserCredentialValueModel cred = null;
for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) { for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
if (model.getType().equals(CREDENTIAL_TYPE)) { 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 @Override
@ -92,6 +91,11 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
@Override @Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG"); user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
}
@Override
public void close() {
} }
} }

View file

@ -21,6 +21,11 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
public static final String PROVIDER_ID = "secret-question-authenticator"; public static final String PROVIDER_ID = "secret-question-authenticator";
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator(); private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
@Override
public String getId() {
return PROVIDER_ID;
}
@Override @Override
public Authenticator create() { public Authenticator create() {
return SINGLETON; return SINGLETON;
@ -31,21 +36,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
return SINGLETON; 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 = { private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED AuthenticationExecutionModel.Requirement.DISABLED
@ -61,8 +51,13 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
} }
@Override @Override
public String getHelpText() { public boolean isConfigurable() {
return "A secret question that a user has to answer. i.e. What is your mother's maiden name."; return true;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
} }
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@ -70,17 +65,27 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
static { static {
ProviderConfigProperty property; ProviderConfigProperty property;
property = new ProviderConfigProperty(); property = new ProviderConfigProperty();
property.setName("remember_machine"); property.setName("cookie.max.age");
property.setLabel("Remember machine"); property.setLabel("Cookie Max Age");
property.setType(ProviderConfigProperty.BOOLEAN_TYPE); property.setType(ProviderConfigProperty.STRING_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.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
configProperties.add(property); configProperties.add(property);
} }
@Override @Override
public List<ProviderConfigProperty> getConfigProperties() { public String getHelpText() {
return configProperties; 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 @Override
@ -98,8 +103,5 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
} }
@Override
public String getId() {
return PROVIDER_ID;
}
} }

View file

@ -0,0 +1,47 @@
package org.keycloak.examples.authenticator;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.UserCredentialValueModel;
import javax.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SecretQuestionRequiredAction implements RequiredActionProvider {
public static final String PROVIDER_ID = "secret_question_config";
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = context.form().createForm("secret_question_config.ftl");
context.challenge(challenge);
}
@Override
public void processAction(RequiredActionContext context) {
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer"));
UserCredentialValueModel model = new UserCredentialValueModel();
model.setValue(answer);
model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
context.getUser().updateCredentialDirectly(model);
context.success();
}
@Override
public String getProviderId() {
return PROVIDER_ID;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.examples.authenticator;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SecretQuestionRequiredActionFactory implements RequiredActionFactory {
private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction();
@Override
public RequiredActionProvider create(KeycloakSession session) {
return SINGLETON;
}
@Override
public String getId() {
return SecretQuestionRequiredAction.PROVIDER_ID;
}
@Override
public String getDisplayText() {
return "Secret Question";
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
}

View file

@ -6,9 +6,6 @@
${msg("loginTitleHtml",realm.name)} ${msg("loginTitleHtml",realm.name)}
<#elseif section = "form"> <#elseif section = "form">
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input id="username" name="username" value="${login.username!''}" type="hidden" />
<input id="password-token" name="password-token" value="${login.passwordToken!''}" type="hidden" />
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label> <label for="totp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label>

View file

@ -1,13 +1,5 @@
package org.keycloak.authentication; 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 * Abstract helper class that Authenticator implementations can leverage
* *
@ -15,53 +7,9 @@ import java.net.URI;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public abstract class AbstractFormAuthenticator implements Authenticator { public abstract class AbstractFormAuthenticator implements Authenticator {
public static final String EXECUTION = "execution";
@Override @Override
public void close() { 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());
}
} }

View file

@ -1,67 +0,0 @@
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() {
}
}

View file

@ -3,6 +3,7 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
@ -14,6 +15,7 @@ import org.keycloak.services.managers.BruteForceProtector;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
/** /**
* This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set * This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set
@ -193,4 +195,26 @@ public interface AuthenticationFlowContext {
* @return may return null if there was no error * @return may return null if there was no error
*/ */
AuthenticationFlowError getError(); AuthenticationFlowError getError();
/**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
*
* @return
*/
LoginFormsProvider form();
/**
* Get the action URL for the required action.
*
* @param code client session access code
* @return
*/
URI getActionUrl(String code);
/**
* Get the action URL for the required action. This auto-generates the access code.
*
* @return
*/
URI getActionUrl();
} }

View file

@ -3,10 +3,12 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
@ -21,10 +23,12 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List; import java.util.List;
/** /**
@ -336,6 +340,33 @@ public class AuthenticationProcessor {
public AuthenticationFlowError getError() { public AuthenticationFlowError getError() {
return error; return error;
} }
@Override
public LoginFormsProvider form() {
String accessCode = generateAccessCode();
URI action = getActionUrl(accessCode);
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setUser(getUser())
.setActionUri(action)
.setClientSessionCode(accessCode);
if (getForwardedErrorMessage() != null) {
provider.setError(getForwardedErrorMessage());
}
return provider;
}
@Override
public URI getActionUrl(String code) {
return LoginActionsService.authenticationFormProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("execution", getExecution().getId())
.build(getRealm().getName());
}
@Override
public URI getActionUrl() {
return getActionUrl(generateAccessCode());
}
} }
public void logFailure() { public void logFailure() {

View file

@ -3,6 +3,7 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -11,6 +12,7 @@ import org.keycloak.models.UserSessionModel;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
/** /**
* Interface that encapsulates current information about the current requred action * Interface that encapsulates current information about the current requred action
@ -19,8 +21,6 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface RequiredActionContext { public interface RequiredActionContext {
void ignore();
public static enum Status { public static enum Status {
CHALLENGE, CHALLENGE,
SUCCESS, SUCCESS,
@ -28,6 +28,37 @@ public interface RequiredActionContext {
FAILURE FAILURE
} }
/**
* Get the action URL for the required action.
*
* @param code client sessino access code
* @return
*/
URI getActionUrl(String code);
/**
* Get the action URL for the required action. This auto-generates the access code.
*
* @return
*/
URI getActionUrl();
/**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
*
* @return
*/
LoginFormsProvider form();
/**
* If challenge has been sent this returns the JAX-RS Response
*
* @return
*/
Response getChallenge();
/** /**
* Current event builder being used * Current event builder being used
* *
@ -59,7 +90,29 @@ public interface RequiredActionContext {
Status getStatus(); Status getStatus();
/**
* Send a challenge Response back to user
*
* @param response
*/
void challenge(Response response); void challenge(Response response);
/**
* Abort the authentication with an error
*
*/
void failure(); void failure();
/**
* Mark this required action as successful. The required action will be removed from the UserModel
*
*/
void success(); void success();
/**
* Ignore this required action and go onto the next, or complete the flow.
*
*/
void ignore();
} }

View file

@ -2,16 +2,20 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.LoginActionsService;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -27,11 +31,12 @@ public class RequiredActionContextResult implements RequiredActionContext {
protected Response challenge; protected Response challenge;
protected HttpRequest httpRequest; protected HttpRequest httpRequest;
protected UserModel user; protected UserModel user;
protected RequiredActionProvider provider;
public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession, public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session, RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
HttpRequest httpRequest, HttpRequest httpRequest,
UserModel user) { UserModel user, RequiredActionProvider provider) {
this.userSession = userSession; this.userSession = userSession;
this.clientSession = clientSession; this.clientSession = clientSession;
this.realm = realm; this.realm = realm;
@ -39,6 +44,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
this.session = session; this.session = session;
this.httpRequest = httpRequest; this.httpRequest = httpRequest;
this.user = user; this.user = user;
this.provider = provider;
} }
@Override @Override
@ -121,6 +127,34 @@ public class RequiredActionContextResult implements RequiredActionContext {
status = Status.IGNORE; status = Status.IGNORE;
} }
@Override
public URI getActionUrl(String code) {
return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("action", provider.getProviderId())
.build(getRealm().getName());
}
@Override
public URI getActionUrl() {
String accessCode = generateAccessCode(provider.getProviderId());
return getActionUrl(accessCode);
}
@Override
public LoginFormsProvider form() {
String accessCode = generateAccessCode(provider.getProviderId());
URI action = getActionUrl(accessCode);
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setUser(getUser())
.setActionUri(action)
.setClientSessionCode(accessCode);
return provider;
}
@Override
public Response getChallenge() { public Response getChallenge() {
return challenge; return challenge;
} }

View file

@ -36,29 +36,29 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} }
protected Response invalidUser(AuthenticationFlowContext context) { protected Response invalidUser(AuthenticationFlowContext context) {
return loginForm(context) return context.form()
.setError(Messages.INVALID_USER) .setError(Messages.INVALID_USER)
.createLogin(); .createLogin();
} }
protected Response disabledUser(AuthenticationFlowContext context) { protected Response disabledUser(AuthenticationFlowContext context) {
return loginForm(context) return context.form()
.setError(Messages.ACCOUNT_DISABLED).createLogin(); .setError(Messages.ACCOUNT_DISABLED).createLogin();
} }
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) { protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
return loginForm(context) return context.form()
.setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin(); .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin();
} }
protected Response invalidCredentials(AuthenticationFlowContext context) { protected Response invalidCredentials(AuthenticationFlowContext context) {
return loginForm(context) return context.form()
.setError(Messages.INVALID_USER).createLogin(); .setError(Messages.INVALID_USER).createLogin();
} }
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) { protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
context.getEvent().error(eventError); context.getEvent().error(eventError);
Response challengeResponse = loginForm(context) Response challengeResponse = context.form()
.setError(loginFormError).createLogin(); .setError(loginFormError).createLogin();
context.failureChallenge(authenticatorError, challengeResponse); context.failureChallenge(authenticatorError, challengeResponse);
return challengeResponse; return challengeResponse;

View file

@ -14,7 +14,6 @@ import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -63,11 +62,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
} }
protected Response challenge(AuthenticationFlowContext context, String error) { protected Response challenge(AuthenticationFlowContext context, String error) {
String accessCode = context.generateAccessCode(); LoginFormsProvider forms = context.form();
URI action = getActionUrl(context, accessCode);
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
.setActionUri(action)
.setClientSessionCode(accessCode);
if (error != null) forms.setError(error); if (error != null) forms.setError(error);
return forms.createLoginTotp(); return forms.createLoginTotp();

View file

@ -117,7 +117,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple
*/ */
protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) { protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) {
String accessCode = context.generateAccessCode(); String accessCode = context.generateAccessCode();
URI action = getActionUrl(context, accessCode); URI action = context.getActionUrl(accessCode);
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();

View file

@ -71,7 +71,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
} }
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) { protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
LoginFormsProvider forms = loginForm(context); LoginFormsProvider forms = context.form();
if (formData.size() > 0) forms.setFormData(formData); if (formData.size() > 0) forms.setFormData(formData);

View file

@ -4,7 +4,6 @@ import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.AbstractFormRequiredAction;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -14,7 +13,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class TermsAndConditions extends AbstractFormRequiredAction implements RequiredActionFactory { public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions"; public static final String PROVIDER_ID = "terms_and_conditions";
@ -54,7 +53,7 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = form(context).createForm("terms.ftl"); Response challenge = context.form().createForm("terms.ftl");
context.challenge(challenge); context.challenge(challenge);
} }
@ -73,4 +72,8 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
return "Terms and Conditions"; return "Terms and Conditions";
} }
@Override
public void close() {
}
} }

View file

@ -423,7 +423,6 @@ public class AuthenticationManager {
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()); logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
@ -433,6 +432,7 @@ public class AuthenticationManager {
for (String action : requiredActions) { for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId()); RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, actionProvider);
actionProvider.requiredActionChallenge(context); actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) { if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
@ -501,32 +501,33 @@ public class AuthenticationManager {
} }
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) { public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user) {
@Override
public void challenge(Response response) {
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
}
@Override
public void failure() {
throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
}
@Override
public void success() {
throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
}
@Override
public void ignore() {
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
}
};
// see if any required actions need triggering, i.e. an expired password // see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue; if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId()); RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, provider) {
@Override
public void challenge(Response response) {
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
}
@Override
public void failure() {
throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
}
@Override
public void success() {
throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
}
@Override
public void ignore() {
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
}
};
provider.evaluateTriggers(result); provider.evaluateTriggers(result);
} }
} }

View file

@ -883,7 +883,7 @@ public class LoginActionsService {
initEvent(clientSession); initEvent(clientSession);
RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser()) { RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), provider) {
@Override @Override
public String generateAccessCode(String action) { public String generateAccessCode(String action) {
String clientSessionAction = clientSession.getAction(); String clientSessionAction = clientSession.getAction();