Merge pull request #1537 from patriot1burke/master
auth spi docs and refactor
This commit is contained in:
commit
39fcd84d53
26 changed files with 337 additions and 153 deletions
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -35,11 +35,6 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
|
|||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -16,6 +16,4 @@ import org.keycloak.provider.ProviderFactory;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface AuthenticatorFactory extends ProviderFactory<Authenticator>, ConfigurableAuthenticatorFactory {
|
||||
Authenticator create();
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,11 +32,6 @@ public abstract class AbstractDirectGrantAuthenticator implements Authenticator,
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -38,13 +38,6 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getProviderId() {
|
||||
return getId();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void evaluateTriggers(RequiredActionContext context) {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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()");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -70,11 +70,6 @@ public class PassThroughRegistration implements Authenticator, AuthenticatorFact
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Dummy Pass Thru";
|
||||
|
|
Loading…
Reference in a new issue