Merge pull request #1537 from patriot1burke/master

auth spi docs and refactor
This commit is contained in:
Bill Burke 2015-08-14 08:53:21 -04:00
commit 39fcd84d53
26 changed files with 337 additions and 153 deletions

View file

@ -370,9 +370,9 @@ Forms Subflow - ALTERNATIVE
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.
The getId() method is just the unique name of the component. The create() method is called by the
runtime to allocate and process the Authenticator.
<programlisting>
public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
@ -384,11 +384,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
return PROVIDER_ID;
}
@Override
public Authenticator create() {
return SINGLETON;
}
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
@ -523,4 +518,253 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
</para>
</section>
</section>
<section>
<title>Required Action Walkthrough</title>
<para>
In this section will discuss how to define a required action. In the Authenticator section you may have wondered,
"How will we get the user's answer to the secret question entered into the system?". As we showed in the example,
if the answer is not set up, a required action will be triggered. This section discusses how to implement
the required action for the Secret Question Authenticator.
</para>
<section>
<title>Packaging Classes and Deployment</title>
<para>
You will package your classes within a single jar. This jar does not have to be separate from other provider classes
but it must contain a file named <literal>org.keycloak.authentication.RequiredActionFactory</literal>
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
of each RequiredActionFactory implementation you have in the jar. For example:
<programlisting>
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
</programlisting>
</para>
<para>
This services/ file is used by Keycloak to scan the providers it has to load into the system.
</para>
<para>
To deploy this jar, just copy it to the standalone/configuration/providers directory.
</para>
</section>
<section>
<title>Implement the RequiredActionProvider</title>
<para>Required actions must first implement the RequiredActionProvider interface. The RequiredActionProvider.requiredActionChallenge()
is the initial call by the flow manager into the required action. This method is responsible for rendering the
HTML form that will drive the required action.
<programlisting>
@Override
public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = context.form().createForm("secret_question_config.ftl");
context.challenge(challenge);
}
</programlisting>
</para>
<para>
You see that RequiredActionContext has similar methods to AuthenticationFlowContext. The form() method allows
you to render the page from a Freemarker template. The action URL is preset by the call to this form() method.
You just need to reference it within your HTML form. I'll show you this later.
</para>
<para>
The challenge() method notifies the flow manager that
a required action must be executed.
</para>
<para>
The next method is responsible for processing input from the HTML form of the required action. The action
URL of the form will be routed to the RequiredActionProvider.processAction() method
<programlisting>
@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();
}
</programlisting>
</para>
<para>
The answer is pulled out of the form post. a UserCredentialValueModel is created and the type and value
of the credential are set. Then UserModel.updateCredentialDirectly() is invoked. Finally, RequiredActionContext.success()
notifies the container that the required action was successful.
</para>
</section>
<section>
<title>Implement the RequiredActionFactory</title>
<para>
This class is really simple. It is just responsible for creating the required actin provider instance.
<programlisting>
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";
}
</programlisting>
</para>
<para>
The getDisplayText() method is just for the admin console when it wants to display a friendly name
for the required action.
</para>
</section>
<section>
<title>Enable Required Action</title>
<para>
The final thing you have to do is go into the admin console. Click on the Authentication left menu.
Click on the Required Actions tab. Find your required action, and enable. Alternatively, if you
click on the default action checkbox, this required action will be applied anytime a new user is created.
</para>
</section>
</section>
<section>
<title>Modifying/Extending the Registration Form</title>
<para>
It is entirely possible for you to implement your own flow with a set of Authenticators to totally change
how regisration is done in Keycloak. But what you'll usually want to do is just add a little be of validation
to the out of the box registration page.
An additional SPI was created to be able to do this. It basically allows
you to add validation of form elements on the page as well as to initialize UserModel attributes and data
after the user has been registered. We'll look at the implementation of the recaptcha support that
Keycloak provides out of the box to show you how to do this.
</para>
<section>
<title>Implementation FormAction Interface</title>
<para>
The core interface you have to implement is the FormAction interface. A FormAction is responsible for
rendering and processing a portion of the page. Rendering is done in the buildPage() method, validation
is done in the validate() method, post validation operations are done in success(). Let's first take a look
at buildPage()
<programlisting><![CDATA[
@Override
public void buildPage(FormContext context, LoginFormsProvider form) {
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
if (captchaConfig == null || captchaConfig.getConfig() == null
|| captchaConfig.getConfig().get(SITE_KEY) == null
|| captchaConfig.getConfig().get(SITE_SECRET) == null
) {
form.addError(new FormMessage(null, Messages.RECAPTCHA_NOT_CONFIGURED));
return;
}
String siteKey = captchaConfig.getConfig().get(SITE_KEY);
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", siteKey);
form.addScript("https://www.google.com/recaptcha/api.js");
}
]]>
</programlisting>
</para>
<para>
The buildPage() method is a callback by the form flow to help render the page. It receives a form parameter
which is a LoginFormsProvider. You can add additional attributes to the form provider so that they can
be displayed in the HTML page generated by the registration Freemarker template.
</para>
<para>
The code above is from the registration recaptcha plugin. Recaptcha requires some specific settings that
must be obtained from configuration. FormActions are configured in the exact same was as Authenticators are.
In this example, we pull the Google Recaptcha site key from configuration and add it as an attribute
to the form provider. Our regstration template file can read this attribute now.
</para>
<para>
Recaptcha also has the requirement of loading a javascript script. You can do this by calling LoginFormsProvider.addScript()
passing in the URL.
</para>
<para>
The next meaty part of this interface is the validate() method. This is called immediately upon receiving a form
post.
<programlisting><![CDATA[
@Override
public void validate(ValidationContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
List<FormMessage> errors = new ArrayList<>();
boolean success = false;
String captcha = formData.getFirst(G_RECAPTCHA_RESPONSE);
if (!Validation.isBlank(captcha)) {
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
String secret = captchaConfig.getConfig().get(SITE_SECRET);
success = validateRecaptcha(context, success, captcha, secret);
}
if (success) {
context.success();
} else {
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
context.validationError(formData, errors);
return;
}
}
]]>
</programlisting>
</para>
<para>
Here we obtain the form data that the Recaptcha widget adds to the form. We obtain the Recaptcha secret key
from configuration. We then validate the recaptcha. If successful, ValidationContext.success() is called.
If not, we invoke ValidationContext.validationError() passing in the formData (so the user doesn't have to re-enter data),
we also specify an error message we want displayed. The error message must point to a message bundle property
in the internationalized message bundles. For other registration extensions validate() might be validating the
format of a form element, i.e. an alternative email attribute.
</para>
<para>
After all validations have been processed then, the form flow then invokes the FormAction.success() method. For recaptcha
this is a no-op, but if you have additional metadata you want to add to UserModel, you can do that in success() method.
</para>
<para>
Finally the FormActionFactory class is really implemented similarly to AuthenticatorFactory, so we won't go over it.
</para>
</section>
<section>
<title>Packaging the Action</title>
<para>
You will package your classes within a single jar. This jar must contain a file named <literal>org.keycloak.authentication.ForActionFactory</literal>
and must be contained in the <literal>META-INF/services/</literal> directory of your jar. This file must list the fully qualified classname
of each FormActionFactory implementation you have in the jar. For example:
<programlisting>
org.keycloak.examples.authenticator.registration.RecaptchaFormActionFactory
</programlisting>
</para>
<para>
This services/ file is used by Keycloak to scan the providers it has to load into the system.
</para>
<para>
To deploy this jar, just copy it to the standalone/configuration/providers directory.
</para>
</section>
<section>
<title>Adding FormAction to the Registration Flow</title>
<para>
Adding an FormAction to a registration page 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 FormAction.
</para>
<para>
Basically you'll have to copy the registration flow. Then click Actions menu to the right of
the Registration Form, and pick "Add Execution" to add a new execution. You'll pick the FormAction from the selection list.
Make sure your FormAction comes after "Registration User Creation" by using the down errors to move it if your FormAction
isn't already listed after "Registration User Creation".
</para>
<para>
After you've created your flow, you have to bind it to registration. If you go
to the Authentication menu and go to the Bindings tab you will see options to bind a flow to
the browser, registration, or direct grant flow.
</para>
</section>
</section>
</chapter>

View file

@ -26,11 +26,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
return PROVIDER_ID;
}
@Override
public Authenticator create() {
return SINGLETON;
}
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;

View file

@ -35,11 +35,6 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
context.success();
}
@Override
public String getProviderId() {
return PROVIDER_ID;
}
@Override
public void close() {

View file

@ -0,0 +1 @@
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory

View file

@ -23,6 +23,13 @@ import org.keycloak.provider.Provider;
*/
public interface LoginFormsProvider extends Provider {
/**
* Adds a script to the html header
*
* @param scriptUrl
*/
void addScript(String scriptUrl);
public Response createResponse(UserModel.RequiredAction action);
Response createForm(String form);

View file

@ -91,6 +91,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
public FreeMarkerLoginFormsProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
this.session = session;
this.freeMarker = freeMarker;
this.attributes.put("scripts", new LinkedList<String>());
}
@Override
public void addScript(String scriptUrl) {
List<String> scripts = (List<String>)this.attributes.get("scripts");
scripts.add(scriptUrl);
}
public Response createResponse(UserModel.RequiredAction action) {

View file

@ -16,6 +16,4 @@ import org.keycloak.provider.ProviderFactory;
* @version $Revision: 1 $
*/
public interface AuthenticatorFactory extends ProviderFactory<Authenticator>, ConfigurableAuthenticatorFactory {
Authenticator create();
}

View file

@ -54,7 +54,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return authenticationFlow.processAction(actionExecution);
} else if (model.getId().equals(actionExecution)) {
AuthenticatorFactory factory = (AuthenticatorFactory) processor.getSession().getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getAuthenticator());
Authenticator authenticator = factory.create();
Authenticator authenticator = factory.create(processor.getSession());
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
authenticator.action(result);
Response response = processResult(result);
@ -108,7 +108,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory == null) {
throw new AuthenticationFlowException("Could not find AuthenticatorFactory for: " + model.getAuthenticator(), AuthenticationFlowError.INTERNAL_ERROR);
}
Authenticator authenticator = factory.create();
Authenticator authenticator = factory.create(processor.getSession());
AuthenticationProcessor.logger.debugv("authenticator: {0}", factory.getId());
UserModel authUser = processor.getClientSession().getAuthenticatedUser();

View file

@ -15,6 +15,14 @@ import org.keycloak.provider.Provider;
* @version $Revision: 1 $
*/
public interface FormAction extends Provider {
/**
* When a FormAuthenticator is rendering the challenge page, even FormAction.buildPage() method will be called
* This gives the FormAction the opportunity to add additional attributes to the form to be displayed.
*
* @param context
* @param form
*/
void buildPage(FormContext context, LoginFormsProvider form);
/**
* This is the first phase of form processing. Each FormAction.validate() method is called. This gives the
* FormAction a chance to validate and challenge if user input is invalid.
@ -53,13 +61,5 @@ public interface FormAction extends Provider {
*/
void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
/**
* When a FormAuthenticator is rendering the challenge page, even FormAction.buildPage() method will be called
* This gives the FormAction the opportunity to add additional attributes to the form to be displayed.
*
* @param context
* @param form
*/
void buildPage(FormContext context, LoginFormsProvider form);
}

View file

@ -31,12 +31,12 @@ public class RequiredActionContextResult implements RequiredActionContext {
protected Response challenge;
protected HttpRequest httpRequest;
protected UserModel user;
protected RequiredActionProvider provider;
protected RequiredActionFactory factory;
public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
HttpRequest httpRequest,
UserModel user, RequiredActionProvider provider) {
UserModel user, RequiredActionFactory factory) {
this.userSession = userSession;
this.clientSession = clientSession;
this.realm = realm;
@ -44,7 +44,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
this.session = session;
this.httpRequest = httpRequest;
this.user = user;
this.provider = provider;
this.factory = factory;
}
@Override
@ -131,20 +131,20 @@ public class RequiredActionContextResult implements RequiredActionContext {
public URI getActionUrl(String code) {
return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam("action", provider.getProviderId())
.queryParam("action", factory.getId())
.build(getRealm().getName());
}
@Override
public URI getActionUrl() {
String accessCode = generateAccessCode(provider.getProviderId());
String accessCode = generateAccessCode(factory.getId());
return getActionUrl(accessCode);
}
@Override
public LoginFormsProvider form() {
String accessCode = generateAccessCode(provider.getProviderId());
String accessCode = generateAccessCode(factory.getId());
URI action = getActionUrl(accessCode);
LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class)
.setUser(getUser())

View file

@ -36,11 +36,4 @@ public interface RequiredActionProvider extends Provider {
* @param context
*/
void processAction(RequiredActionContext context);
/**
* Provider id of this required action. Must match ProviderFactory.getId().
*
* @return
*/
String getProviderId();
}

View file

@ -17,14 +17,10 @@ import java.util.List;
public class CookieAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-cookie";
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
@Override
public Authenticator create() {
return SINGLETON;
}
@Override
public Authenticator create(KeycloakSession session) {
throw new IllegalStateException("illegal call");
return SINGLETON;
}
@Override

View file

@ -18,15 +18,11 @@ import java.util.List;
public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-otp-form";
@Override
public Authenticator create() {
return new OTPFormAuthenticator();
}
public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
throw new IllegalStateException("illegal call");
return SINGLETON;
}
@Override

View file

@ -18,15 +18,11 @@ import java.util.List;
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-spnego";
@Override
public Authenticator create() {
return new SpnegoAuthenticator();
}
public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
throw new IllegalStateException("illegal call");
return SINGLETON;
}
@Override

View file

@ -18,15 +18,11 @@ import java.util.List;
public class UsernamePasswordFormFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-username-password-form";
@Override
public Authenticator create() {
return new UsernamePasswordForm();
}
public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
@Override
public Authenticator create(KeycloakSession session) {
throw new IllegalStateException("illegal call");
return SINGLETON;
}
@Override

View file

@ -32,11 +32,6 @@ public abstract class AbstractDirectGrantAuthenticator implements Authenticator,
}
@Override
public Authenticator create() {
return this;
}
@Override
public void close() {

View file

@ -86,9 +86,7 @@ public class RegistrationRecaptcha implements FormAction, FormActionFactory, Con
String siteKey = captchaConfig.getConfig().get(SITE_KEY);
form.setAttribute("recaptchaRequired", true);
form.setAttribute("recaptchaSiteKey", siteKey);
List<String> scripts = new LinkedList<>();
scripts.add("https://www.google.com/recaptcha/api.js");
form.setAttribute("scripts", scripts);
form.addScript("https://www.google.com/recaptcha/api.js");
}
@Override
@ -103,27 +101,7 @@ public class RegistrationRecaptcha implements FormAction, FormActionFactory, Con
AuthenticatorConfigModel captchaConfig = context.getAuthenticatorConfig();
String secret = captchaConfig.getConfig().get(SITE_SECRET);
HttpClient httpClient = context.getSession().getProvider(HttpClientProvider.class).getHttpClient();
HttpPost post = new HttpPost("https://www.google.com/recaptcha/api/siteverify");
List<NameValuePair> formparams = new LinkedList<>();
formparams.add(new BasicNameValuePair("secret", secret));
formparams.add(new BasicNameValuePair("response", captcha));
formparams.add(new BasicNameValuePair("remoteip", context.getConnection().getRemoteAddr()));
try {
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
InputStream content = response.getEntity().getContent();
try {
Map json = JsonSerialization.readValue(content, Map.class);
Object val = json.get("success");
success = Boolean.TRUE.equals(val);
} finally {
content.close();
}
} catch (Exception e) {
logger.error("Recaptcha failed", e);
}
success = validateRecaptcha(context, success, captcha, secret);
}
if (success) {
context.success();
@ -138,6 +116,31 @@ public class RegistrationRecaptcha implements FormAction, FormActionFactory, Con
}
}
protected boolean validateRecaptcha(ValidationContext context, boolean success, String captcha, String secret) {
HttpClient httpClient = context.getSession().getProvider(HttpClientProvider.class).getHttpClient();
HttpPost post = new HttpPost("https://www.google.com/recaptcha/api/siteverify");
List<NameValuePair> formparams = new LinkedList<>();
formparams.add(new BasicNameValuePair("secret", secret));
formparams.add(new BasicNameValuePair("response", captcha));
formparams.add(new BasicNameValuePair("remoteip", context.getConnection().getRemoteAddr()));
try {
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = httpClient.execute(post);
InputStream content = response.getEntity().getContent();
try {
Map json = JsonSerialization.readValue(content, Map.class);
Object val = json.get("success");
success = Boolean.TRUE.equals(val);
} finally {
content.close();
}
} catch (Exception e) {
logger.error("Recaptcha failed", e);
}
return success;
}
@Override
public void success(FormContext context) {

View file

@ -38,13 +38,6 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
}
@Override
public String getProviderId() {
return getId();
}
@Override
public void evaluateTriggers(RequiredActionContext context) {

View file

@ -51,7 +51,7 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
public void requiredActionChallenge(RequiredActionContext context) {
LoginFormsProvider loginFormsProvider = context.getSession()
.getProvider(LoginFormsProvider.class)
.setClientSessionCode(context.generateAccessCode(getProviderId()))
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.UPDATE_PASSWORD.name()))
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
context.challenge(challenge);
@ -92,10 +92,4 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
public String getId() {
return UserModel.RequiredAction.UPDATE_PASSWORD.name();
}
@Override
public String getProviderId() {
return getId();
}
}

View file

@ -25,7 +25,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
@Override
public void requiredActionChallenge(RequiredActionContext context) {
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(context.generateAccessCode(getProviderId()))
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.UPDATE_PROFILE.name()))
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
@ -67,10 +67,4 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
public String getId() {
return UserModel.RequiredAction.UPDATE_PROFILE.name();
}
@Override
public String getProviderId() {
return getId();
}
}

View file

@ -27,7 +27,7 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
@Override
public void requiredActionChallenge(RequiredActionContext context) {
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(context.generateAccessCode(getProviderId()))
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.CONFIGURE_TOTP.name()))
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
context.challenge(challenge);
@ -69,11 +69,4 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
public String getId() {
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
}
@Override
public String getProviderId() {
return getId();
}
}

View file

@ -44,7 +44,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId());
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(context.generateAccessCode(getProviderId()))
.setClientSessionCode(context.generateAccessCode(UserModel.RequiredAction.VERIFY_EMAIL.name()))
.setUser(context.getUser());
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
context.challenge(challenge);
@ -86,11 +86,4 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
public String getId() {
return UserModel.RequiredAction.VERIFY_EMAIL.name();
}
@Override
public String getProviderId() {
return getId();
}
}

View file

@ -8,6 +8,7 @@ import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.events.Details;
@ -431,8 +432,9 @@ public class AuthenticationManager {
Set<String> requiredActions = user.getRequiredActions();
for (String action : requiredActions) {
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, actionProvider);
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
RequiredActionProvider actionProvider = factory.create(session);
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context);
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
@ -447,8 +449,8 @@ public class AuthenticationManager {
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());
event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success();
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
}
}
if (client.isConsentRequired()) {
@ -505,8 +507,9 @@ public class AuthenticationManager {
// see if any required actions need triggering, i.e. an expired password
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
if (!model.isEnabled()) continue;
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, provider) {
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, model.getProviderId());
RequiredActionProvider provider = factory.create(session);
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) {
@Override
public void challenge(Response response) {
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");

View file

@ -27,6 +27,7 @@ import org.keycloak.ClientConnection;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
@ -861,12 +862,13 @@ public class LoginActionsService {
}
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, action);
if (provider == null) {
RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action);
if (factory == null) {
logger.error("required action provider was null");
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE));
}
RequiredActionProvider provider = factory.create(session);
Checks checks = new Checks();
if (!checks.verifyCode(realm.getBrowserFlow(), code, action)) {
return checks.response;
@ -883,7 +885,7 @@ public class LoginActionsService {
initEvent(clientSession);
RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), provider) {
RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), factory) {
@Override
public String generateAccessCode(String action) {
String clientSessionAction = clientSession.getAction();
@ -905,7 +907,7 @@ public class LoginActionsService {
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());
clientSession.getUserSession().getUser().removeRequiredAction(factory.getId());
return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
}
if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {

View file

@ -54,12 +54,7 @@ public class PassThroughAuthenticator implements Authenticator, AuthenticatorFac
}
@Override
public Authenticator create() {
return this;
}
@Override
@Override
public String getDisplayType() {
return "Dummy Pass Thru";
}

View file

@ -70,11 +70,6 @@ public class PassThroughRegistration implements Authenticator, AuthenticatorFact
}
@Override
public Authenticator create() {
return this;
}
@Override
public String getDisplayType() {
return "Dummy Pass Thru";