auth spi refactor and doco
This commit is contained in:
parent
523bdd5ec9
commit
8a23463328
20 changed files with 520 additions and 204 deletions
|
@ -226,6 +226,9 @@ Forms Subflow - ALTERNATIVE
|
|||
<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>Implementing an Authenticator</title>
|
||||
|
@ -293,8 +296,7 @@ Forms Subflow - ALTERNATIVE
|
|||
</para>
|
||||
<para>
|
||||
If the hasCookie() method returns false, we must return a response that renders the secret question HTML
|
||||
form. If your Authenticator classes inherit from the helper class <literal>org.keycloak.authentication.AbstractFormAuthenticator</literal>
|
||||
it has a loginForm() method that initializes a Freemarker page builder with appropriate base information needed
|
||||
form. AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed
|
||||
to build the form. This page builder is called <literal>org.keycloak.login.LoginFormsProvider</literal>.
|
||||
the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme. Additionally
|
||||
you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the
|
||||
|
@ -314,9 +316,9 @@ Forms Subflow - ALTERNATIVE
|
|||
public void action(AuthenticationFlowContext context) {
|
||||
boolean validated = validateAnswer(context);
|
||||
if (!validated) {
|
||||
Response challenge = loginForm(context)
|
||||
Response challenge = context.form()
|
||||
.setError("badSecret")
|
||||
.createForm("secret_question.ftl");
|
||||
.createForm("secret-question.ftl");
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
|
||||
return;
|
||||
}
|
||||
|
@ -360,5 +362,165 @@ Forms Subflow - ALTERNATIVE
|
|||
defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
|
||||
</para>
|
||||
</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>
|
||||
</chapter>
|
34
examples/providers/authenticator/secret-question.ftl
Executable file
34
examples/providers/authenticator/secret-question.ftl
Executable 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>
|
|
@ -2,13 +2,12 @@ package org.keycloak.examples.authenticator;
|
|||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialValueModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.CredentialValidation;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
|
||||
import javax.ws.rs.core.Cookie;
|
||||
|
@ -19,7 +18,7 @@ import javax.ws.rs.core.Response;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
||||
public class SecretQuestionAuthenticator implements Authenticator {
|
||||
|
||||
public static final String CREDENTIAL_TYPE = "secret_question";
|
||||
|
||||
|
@ -34,7 +33,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
|||
context.success();
|
||||
return;
|
||||
}
|
||||
Response challenge = loginForm(context).createForm("secret_question.ftl");
|
||||
Response challenge = context.form().createForm("secret_question.ftl");
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
|
@ -42,9 +41,9 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
|||
public void action(AuthenticationFlowContext context) {
|
||||
boolean validated = validateAnswer(context);
|
||||
if (!validated) {
|
||||
Response challenge = loginForm(context)
|
||||
Response challenge = context.form()
|
||||
.setError("badSecret")
|
||||
.createForm("secret_question.ftl");
|
||||
.createForm("secret-question.ftl");
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
|
||||
return;
|
||||
}
|
||||
|
@ -68,7 +67,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
|||
|
||||
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
String secret = formData.getFirst("secret");
|
||||
String secret = formData.getFirst("secret_answer");
|
||||
UserCredentialValueModel cred = null;
|
||||
for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
|
||||
if (model.getType().equals(CREDENTIAL_TYPE)) {
|
||||
|
@ -77,7 +76,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
|||
}
|
||||
}
|
||||
|
||||
return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
|
||||
return cred.getValue().equals(secret);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,6 +91,11 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator {
|
|||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG");
|
||||
user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
public static final String PROVIDER_ID = "secret-question-authenticator";
|
||||
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create() {
|
||||
return SINGLETON;
|
||||
|
@ -31,21 +36,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Secret Question";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "Secret Question";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
|
@ -61,8 +51,13 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
|
||||
public boolean isConfigurable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
@ -70,17 +65,27 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName("remember_machine");
|
||||
property.setLabel("Remember machine");
|
||||
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||
property.setHelpText("If set to true, a checkbox will appear when entering in secret on whether the user wants keycloak to remember the machine. If the user wants to remember, then a persistent cookie is set, and the user will not have to enter in their secret again.");
|
||||
property.setName("cookie.max.age");
|
||||
property.setLabel("Cookie Max Age");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE.");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
public String getHelpText() {
|
||||
return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Secret Question";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return "Secret Question";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,8 +103,5 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory,
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -6,9 +6,6 @@
|
|||
${msg("loginTitleHtml",realm.name)}
|
||||
<#elseif section = "form">
|
||||
<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.kcLabelWrapperClass!}">
|
||||
<label for="totp" class="${properties.kcLabelClass!}">${msg("loginTotpOneTime")}</label>
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
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
|
||||
*
|
||||
|
@ -15,53 +7,9 @@ import java.net.URI;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractFormAuthenticator implements Authenticator {
|
||||
public static final String EXECUTION = "execution";
|
||||
|
||||
@Override
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@ package org.keycloak.authentication;
|
|||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
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.UriInfo;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,12 @@ package org.keycloak.authentication;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
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.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -336,6 +340,33 @@ public class AuthenticationProcessor {
|
|||
public AuthenticationFlowError getError() {
|
||||
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() {
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.keycloak.authentication;
|
|||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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.UriInfo;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Interface that encapsulates current information about the current requred action
|
||||
|
@ -19,8 +21,6 @@ import javax.ws.rs.core.UriInfo;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface RequiredActionContext {
|
||||
void ignore();
|
||||
|
||||
public static enum Status {
|
||||
CHALLENGE,
|
||||
SUCCESS,
|
||||
|
@ -28,6 +28,37 @@ public interface RequiredActionContext {
|
|||
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
|
||||
*
|
||||
|
@ -59,7 +90,29 @@ public interface RequiredActionContext {
|
|||
|
||||
Status getStatus();
|
||||
|
||||
/**
|
||||
* Send a challenge Response back to user
|
||||
*
|
||||
* @param response
|
||||
*/
|
||||
void challenge(Response response);
|
||||
|
||||
/**
|
||||
* Abort the authentication with an error
|
||||
*
|
||||
*/
|
||||
void failure();
|
||||
|
||||
/**
|
||||
* Mark this required action as successful. The required action will be removed from the UserModel
|
||||
*
|
||||
*/
|
||||
void success();
|
||||
|
||||
/**
|
||||
* Ignore this required action and go onto the next, or complete the flow.
|
||||
*
|
||||
*/
|
||||
void ignore();
|
||||
|
||||
}
|
||||
|
|
|
@ -2,16 +2,20 @@ package org.keycloak.authentication;
|
|||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -27,11 +31,12 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
protected Response challenge;
|
||||
protected HttpRequest httpRequest;
|
||||
protected UserModel user;
|
||||
protected RequiredActionProvider provider;
|
||||
|
||||
public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
|
||||
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
|
||||
HttpRequest httpRequest,
|
||||
UserModel user) {
|
||||
UserModel user, RequiredActionProvider provider) {
|
||||
this.userSession = userSession;
|
||||
this.clientSession = clientSession;
|
||||
this.realm = realm;
|
||||
|
@ -39,6 +44,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
this.session = session;
|
||||
this.httpRequest = httpRequest;
|
||||
this.user = user;
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -121,6 +127,34 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
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() {
|
||||
return challenge;
|
||||
}
|
||||
|
|
|
@ -36,29 +36,29 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
|||
}
|
||||
|
||||
protected Response invalidUser(AuthenticationFlowContext context) {
|
||||
return loginForm(context)
|
||||
return context.form()
|
||||
.setError(Messages.INVALID_USER)
|
||||
.createLogin();
|
||||
}
|
||||
|
||||
protected Response disabledUser(AuthenticationFlowContext context) {
|
||||
return loginForm(context)
|
||||
return context.form()
|
||||
.setError(Messages.ACCOUNT_DISABLED).createLogin();
|
||||
}
|
||||
|
||||
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
|
||||
return loginForm(context)
|
||||
return context.form()
|
||||
.setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin();
|
||||
}
|
||||
|
||||
protected Response invalidCredentials(AuthenticationFlowContext context) {
|
||||
return loginForm(context)
|
||||
return context.form()
|
||||
.setError(Messages.INVALID_USER).createLogin();
|
||||
}
|
||||
|
||||
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
|
||||
context.getEvent().error(eventError);
|
||||
Response challengeResponse = loginForm(context)
|
||||
Response challengeResponse = context.form()
|
||||
.setError(loginFormError).createLogin();
|
||||
context.failureChallenge(authenticatorError, challengeResponse);
|
||||
return challengeResponse;
|
||||
|
|
|
@ -14,7 +14,6 @@ import org.keycloak.services.messages.Messages;
|
|||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -63,11 +62,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
|
|||
}
|
||||
|
||||
protected Response challenge(AuthenticationFlowContext context, String error) {
|
||||
String accessCode = context.generateAccessCode();
|
||||
URI action = getActionUrl(context, accessCode);
|
||||
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setActionUri(action)
|
||||
.setClientSessionCode(accessCode);
|
||||
LoginFormsProvider forms = context.form();
|
||||
if (error != null) forms.setError(error);
|
||||
|
||||
return forms.createLoginTotp();
|
||||
|
|
|
@ -117,7 +117,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple
|
|||
*/
|
||||
protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) {
|
||||
String accessCode = context.generateAccessCode();
|
||||
URI action = getActionUrl(context, accessCode);
|
||||
URI action = context.getActionUrl(accessCode);
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
|
|||
}
|
||||
|
||||
protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
|
||||
LoginFormsProvider forms = loginForm(context);
|
||||
LoginFormsProvider forms = context.form();
|
||||
|
||||
if (formData.size() > 0) forms.setFormData(formData);
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import org.keycloak.Config;
|
|||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.authentication.AbstractFormRequiredAction;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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>
|
||||
* @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";
|
||||
|
||||
|
@ -54,7 +53,7 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
|
|||
|
||||
@Override
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
Response challenge = form(context).createForm("terms.ftl");
|
||||
Response challenge = context.form().createForm("terms.ftl");
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
|
@ -73,4 +72,8 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re
|
|||
return "Terms and Conditions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -423,7 +423,6 @@ public class AuthenticationManager {
|
|||
|
||||
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());
|
||||
|
||||
|
@ -433,6 +432,7 @@ public class AuthenticationManager {
|
|||
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);
|
||||
actionProvider.requiredActionChallenge(context);
|
||||
|
||||
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
||||
|
@ -501,7 +501,12 @@ 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) {
|
||||
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user) {
|
||||
|
||||
// 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) {
|
||||
@Override
|
||||
public void challenge(Response response) {
|
||||
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
|
||||
|
@ -523,10 +528,6 @@ 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());
|
||||
provider.evaluateTriggers(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -883,7 +883,7 @@ public class LoginActionsService {
|
|||
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
|
||||
public String generateAccessCode(String action) {
|
||||
String clientSessionAction = clientSession.getAction();
|
||||
|
|
Loading…
Reference in a new issue