Merge pull request #1532 from patriot1burke/master
refactor requiredactions, start doco
This commit is contained in:
commit
58e37b6a3f
36 changed files with 1030 additions and 264 deletions
80
docbook/reference/en/en-US/modules/auth-spi.xml
Executable file
80
docbook/reference/en/en-US/modules/auth-spi.xml
Executable file
|
@ -0,0 +1,80 @@
|
|||
<chapter id="auth_spi">
|
||||
<title>Custom Authentication, Registration, and Required Actions</title>
|
||||
<para>
|
||||
Keycloak comes out of the box with a bunch of different authentication mechanisms: kerberos, password, and otp.
|
||||
These mechanisms may not meet all of your requirements and you may want to plug in your own custom ones. Keycloak
|
||||
provides an authentication SPI that you can use to write new plugins. The admin console supports applying, ordering,
|
||||
and configuring these new mechanisms.
|
||||
</para>
|
||||
<para>
|
||||
Keycloak also supports a simple registration form. Different aspects of this form can be enabled and disabled i.e.
|
||||
Recaptcha support can be turned off and on. The same authentication SPI can be used to add another page to the
|
||||
registration flow or reimplement it entirely. There's also an additional fine-grain SPI you can use to add
|
||||
specific validations and user extensions to the built in registration form.
|
||||
</para>
|
||||
<para>
|
||||
A required action in Keycloak is an action that a user has to perform after he authenticates. After the action
|
||||
is performed successfully, the user doesn't have to perform the action again. Keycloak comes with some built in
|
||||
required actions like "reset password". This action forces the user to change their password after they have logged in.
|
||||
You can write and plug in your own required actions.
|
||||
</para>
|
||||
<section>
|
||||
<title>Terms</title>
|
||||
<para>
|
||||
To first learn about the Authentication SPI, let's go over some of the terms used to describe it.
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term>Authentication Flow</term>
|
||||
<listitem>
|
||||
<para>
|
||||
A flow is a container for all authentications that must happen during login or registration. If you
|
||||
go to the admin console authentication page, you can view all the defined flows in the system and
|
||||
what authenticators they are made up of. Flows can contain other flows. You can also bind a new
|
||||
different flow for browser login, direct granta access, and registration.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Authenticator</term>
|
||||
<listitem>
|
||||
<para>
|
||||
An authenticator is a pluggable component that hold the logic for performing the authentication
|
||||
or action within a flow. It is usually a singleton.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Execution</term>
|
||||
<listitem>
|
||||
<para>
|
||||
An execution is an object that binds the authenticator to the flow and the authenticator
|
||||
to the configuration of the authenticator. Flows contain execution entries.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Execution Requirement</term>
|
||||
<listitem>
|
||||
<para>
|
||||
Each execution defines how an authenticator behaves in a flow. The requirement defines
|
||||
whether the authenticator is enabled, disabled, optional, required, or an alternative. An
|
||||
alternative requirement means that the authentiactor is optional unless no other alternative
|
||||
authenticator is successful in the flow. For example, cookie authentication, kerberos,
|
||||
and the set of all login forms are all alternative. If one of those is successful, none of
|
||||
the others are executed.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>Authenticator Config</term>
|
||||
<listitem>
|
||||
<para>
|
||||
This object defines the configuration for the Authenticator for a specific execution within
|
||||
an authentication flow. Each execution can have a different config.
|
||||
</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
|
@ -4,7 +4,7 @@ package org.keycloak.events;
|
|||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public interface Details {
|
||||
|
||||
String CUSTOM_REQUIRED_ACTION="custom_required_action";
|
||||
String EMAIL = "email";
|
||||
String PREVIOUS_EMAIL = "previous_email";
|
||||
String UPDATED_EMAIL = "updated_email";
|
||||
|
|
|
@ -66,7 +66,8 @@ public enum EventType {
|
|||
IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false),
|
||||
IDENTITY_PROVIDER_ACCCOUNT_LINKING(false),
|
||||
IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false),
|
||||
IMPERSONATE(true);
|
||||
IMPERSONATE(true),
|
||||
CUSTOM_REQUIRED_ACTION(true);
|
||||
|
||||
private boolean saveByDefault;
|
||||
|
||||
|
|
27
examples/providers/authenticator/README.md
Executable file
27
examples/providers/authenticator/README.md
Executable file
|
@ -0,0 +1,27 @@
|
|||
Example User Federation Provider
|
||||
===================================================
|
||||
|
||||
This is an example of user federation backed by a simple properties file. This properties file only contains username/password
|
||||
key pairs. To deploy, build this directory then take the jar and copy it to standalone/configuration/providers. Alternatively you can deploy as a module by running:
|
||||
|
||||
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.userprops --resources=target/federation-properties-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api"
|
||||
|
||||
Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
|
||||
|
||||
"providers": [
|
||||
....
|
||||
"module:org.keycloak.examples.userprops"
|
||||
],
|
||||
|
||||
|
||||
You will then have to restart the authentication server.
|
||||
|
||||
The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation
|
||||
page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you
|
||||
specify a classpath to a properties file in the "path" field of the admin page for this plugin. This example includes
|
||||
a "test-users.properties" within the JAR that you can use as the variable.
|
||||
|
||||
The FilePropertiesFederationProvider is an example of a writable provider. It synchronizes changes made to
|
||||
username and password with the properties file. If you go to the Users/Federation page of the admin console you will
|
||||
see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to
|
||||
a properties file in the "path" field of the admin page for this plugin.
|
47
examples/providers/authenticator/pom.xml
Executable file
47
examples/providers/authenticator/pom.xml
Executable file
|
@ -0,0 +1,47 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>keycloak-examples-providers-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>1.5.0.Final-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Authenticator Example</name>
|
||||
<description/>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>authenticator-example</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-model-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-login-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>federation-properties-example</finalName>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,80 @@
|
|||
package org.keycloak.examples.authenticator;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AbstractFormAuthenticator;
|
||||
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 javax.ws.rs.core.MultivaluedMap;
|
||||
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 static final String CREDENTIAL_TYPE = "secret_question";
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
Response challenge = loginForm(context).createForm("secret_question.ftl");
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
String secret = formData.getFirst("secret");
|
||||
if (secret == null || secret.trim().equals("")) {
|
||||
badSecret(context);
|
||||
return;
|
||||
}
|
||||
|
||||
UserCredentialValueModel cred = null;
|
||||
for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) {
|
||||
if (model.getType().equals(CREDENTIAL_TYPE)) {
|
||||
cred = model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cred == null) {
|
||||
badSecret(context);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean validated = CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred);
|
||||
if (!validated) {
|
||||
badSecret(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.success();
|
||||
}
|
||||
|
||||
private void badSecret(AuthenticationFlowContext context) {
|
||||
Response challenge = loginForm(context)
|
||||
.setError("badSecret")
|
||||
.createForm("secret_question.ftl");
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return session.users().configuredForCredentialType(CREDENTIAL_TYPE, realm, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package org.keycloak.examples.authenticator;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "secret-question-authenticator";
|
||||
private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator();
|
||||
|
||||
@Override
|
||||
public Authenticator create() {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
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
|
||||
};
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "A secret question that a user has to answer. i.e. What is your mother's maiden name.";
|
||||
}
|
||||
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
|
||||
|
||||
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.");
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory
|
|
@ -17,5 +17,6 @@
|
|||
<module>event-listener-sysout</module>
|
||||
<module>event-store-mem</module>
|
||||
<module>federation-provider</module>
|
||||
<module>authenticator</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
<div id="kc-terms-text">
|
||||
${msg("termsText")}
|
||||
</div>
|
||||
<form class="form-actions" action="${requiredActionUrl("terms_and_conditions", "")}" method="POST">
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-login" type="submit" value="${msg("doAccept")}"/>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doDecline")}"/>
|
||||
<form class="form-actions" action="${url.loginAction}" method="POST">
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doAccept")}"/>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-decline" type="submit" value="${msg("doDecline")}"/>
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -25,7 +25,7 @@ public interface LoginFormsProvider extends Provider {
|
|||
|
||||
public Response createResponse(UserModel.RequiredAction action);
|
||||
|
||||
Response createForm(String form, Map<String, Object> attributes);
|
||||
Response createForm(String form);
|
||||
|
||||
public Response createLogin();
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response createForm(String form, Map<String, Object> extraAttributes) {
|
||||
public Response createForm(String form) {
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel client = session.getContext().getClient();
|
||||
|
|
|
@ -38,29 +38,34 @@ public class CredentialValidation {
|
|||
* @return
|
||||
*/
|
||||
public static boolean validPassword(RealmModel realm, UserModel user, String password) {
|
||||
boolean validated = false;
|
||||
UserCredentialValueModel passwordCred = null;
|
||||
for (UserCredentialValueModel cred : user.getCredentialsDirectly()) {
|
||||
if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
|
||||
validated = new Pbkdf2PasswordEncoder(cred.getSalt()).verify(password, cred.getValue(), cred.getHashIterations());
|
||||
passwordCred = cred;
|
||||
}
|
||||
}
|
||||
if (passwordCred == null) return false;
|
||||
|
||||
return validateHashedCredential(realm, user, password, passwordCred);
|
||||
|
||||
}
|
||||
|
||||
public static boolean validateHashedCredential(RealmModel realm, UserModel user, String unhashedCredValue, UserCredentialValueModel credential) {
|
||||
boolean validated = new Pbkdf2PasswordEncoder(credential.getSalt()).verify(unhashedCredValue, credential.getValue(), credential.getHashIterations());
|
||||
if (validated) {
|
||||
int iterations = hashIterations(realm);
|
||||
if (iterations > -1 && iterations != passwordCred.getHashIterations()) {
|
||||
if (iterations > -1 && iterations != credential.getHashIterations()) {
|
||||
UserCredentialValueModel newCred = new UserCredentialValueModel();
|
||||
newCred.setType(passwordCred.getType());
|
||||
newCred.setDevice(passwordCred.getDevice());
|
||||
newCred.setSalt(passwordCred.getSalt());
|
||||
newCred.setType(credential.getType());
|
||||
newCred.setDevice(credential.getDevice());
|
||||
newCred.setSalt(credential.getSalt());
|
||||
newCred.setHashIterations(iterations);
|
||||
newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(password, iterations));
|
||||
newCred.setValue(new Pbkdf2PasswordEncoder(newCred.getSalt()).encode(unhashedCredValue, iterations));
|
||||
user.updateCredentialDirectly(newCred);
|
||||
}
|
||||
|
||||
}
|
||||
return validated;
|
||||
|
||||
}
|
||||
|
||||
public static boolean validPasswordToken(RealmModel realm, UserModel user, String encodedPasswordToken) {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
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
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
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() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -16,6 +16,10 @@ import javax.ws.rs.core.Response;
|
|||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set
|
||||
* the status of the execution being performed.
|
||||
*
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
@ -121,6 +125,7 @@ public interface AuthenticationFlowContext {
|
|||
|
||||
AuthenticationExecutionModel.Requirement getCategoryRequirementFromCurrentFlow(String authenticatorCategory);
|
||||
|
||||
|
||||
/**
|
||||
* Mark the current execution as successful. The flow will then continue
|
||||
*
|
||||
|
@ -174,4 +179,18 @@ public interface AuthenticationFlowContext {
|
|||
*
|
||||
*/
|
||||
void attempted();
|
||||
|
||||
/**
|
||||
* Get the current status of the current execution.
|
||||
*
|
||||
* @return may return null if not set yet.
|
||||
*/
|
||||
FlowStatus getStatus();
|
||||
|
||||
/**
|
||||
* Get the error condition of a failed execution.
|
||||
*
|
||||
* @return may return null if there was no error
|
||||
*/
|
||||
AuthenticationFlowError getError();
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.keycloak.authentication;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
|
@ -53,16 +53,6 @@ public class AuthenticationProcessor {
|
|||
protected boolean userSessionCreated;
|
||||
|
||||
|
||||
public static enum Status {
|
||||
SUCCESS,
|
||||
CHALLENGE,
|
||||
FORCE_CHALLENGE,
|
||||
FAILURE_CHALLENGE,
|
||||
FAILED,
|
||||
ATTEMPTED
|
||||
|
||||
}
|
||||
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
@ -173,7 +163,7 @@ public class AuthenticationProcessor {
|
|||
AuthenticatorConfigModel authenticatorConfig;
|
||||
AuthenticationExecutionModel execution;
|
||||
Authenticator authenticator;
|
||||
Status status;
|
||||
FlowStatus status;
|
||||
Response challenge;
|
||||
AuthenticationFlowError error;
|
||||
List<AuthenticationExecutionModel> currentExecutions;
|
||||
|
@ -219,32 +209,33 @@ public class AuthenticationProcessor {
|
|||
return authenticator;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
@Override
|
||||
public FlowStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success() {
|
||||
this.status = Status.SUCCESS;
|
||||
this.status = FlowStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(AuthenticationFlowError error) {
|
||||
status = Status.FAILED;
|
||||
status = FlowStatus.FAILED;
|
||||
this.error = error;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void challenge(Response challenge) {
|
||||
this.status = Status.CHALLENGE;
|
||||
this.status = FlowStatus.CHALLENGE;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceChallenge(Response challenge) {
|
||||
this.status = Status.FORCE_CHALLENGE;
|
||||
this.status = FlowStatus.FORCE_CHALLENGE;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
|
@ -252,7 +243,7 @@ public class AuthenticationProcessor {
|
|||
@Override
|
||||
public void failureChallenge(AuthenticationFlowError error, Response challenge) {
|
||||
this.error = error;
|
||||
this.status = Status.FAILURE_CHALLENGE;
|
||||
this.status = FlowStatus.FAILURE_CHALLENGE;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
|
@ -260,14 +251,14 @@ public class AuthenticationProcessor {
|
|||
@Override
|
||||
public void failure(AuthenticationFlowError error, Response challenge) {
|
||||
this.error = error;
|
||||
this.status = Status.FAILED;
|
||||
this.status = FlowStatus.FAILED;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attempted() {
|
||||
this.status = Status.ATTEMPTED;
|
||||
this.status = FlowStatus.ATTEMPTED;
|
||||
|
||||
}
|
||||
|
||||
|
@ -341,6 +332,7 @@ public class AuthenticationProcessor {
|
|||
return challenge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationFlowError getError() {
|
||||
return error;
|
||||
}
|
||||
|
@ -348,7 +340,7 @@ public class AuthenticationProcessor {
|
|||
|
||||
public void logFailure() {
|
||||
if (realm.isBruteForceProtected()) {
|
||||
String username = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
|
||||
String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
|
||||
// todo need to handle non form failures
|
||||
if (username == null) {
|
||||
|
||||
|
@ -513,7 +505,7 @@ public class AuthenticationProcessor {
|
|||
|
||||
public void attachSession() {
|
||||
String username = clientSession.getAuthenticatedUser().getUsername();
|
||||
String attemptedUsername = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
|
||||
String attemptedUsername = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
|
||||
if (attemptedUsername != null) username = attemptedUsername;
|
||||
if (userSession == null) { // if no authenticator attached a usersession
|
||||
boolean remember = "true".equals(clientSession.getNote(Details.REMEMBER_ME));
|
||||
|
|
|
@ -149,13 +149,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
|
||||
public Response processResult(AuthenticationProcessor.Result result) {
|
||||
AuthenticationExecutionModel execution = result.getExecution();
|
||||
AuthenticationProcessor.Status status = result.getStatus();
|
||||
if (status == AuthenticationProcessor.Status.SUCCESS) {
|
||||
FlowStatus status = result.getStatus();
|
||||
if (status == FlowStatus.SUCCESS) {
|
||||
AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
|
||||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
|
||||
if (execution.isAlternative()) alternativeSuccessful = true;
|
||||
return null;
|
||||
} else if (status == AuthenticationProcessor.Status.FAILED) {
|
||||
} else if (status == FlowStatus.FAILED) {
|
||||
AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
|
||||
processor.logFailure();
|
||||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
|
||||
|
@ -163,10 +163,10 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
return sendChallenge(result, execution);
|
||||
}
|
||||
throw new AuthenticationFlowException(result.getError());
|
||||
} else if (status == AuthenticationProcessor.Status.FORCE_CHALLENGE) {
|
||||
} else if (status == FlowStatus.FORCE_CHALLENGE) {
|
||||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
|
||||
return sendChallenge(result, execution);
|
||||
} else if (status == AuthenticationProcessor.Status.CHALLENGE) {
|
||||
} else if (status == FlowStatus.CHALLENGE) {
|
||||
AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
|
||||
if (execution.isRequired()) {
|
||||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
|
||||
|
@ -184,12 +184,12 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|
|||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
|
||||
}
|
||||
return null;
|
||||
} else if (status == AuthenticationProcessor.Status.FAILURE_CHALLENGE) {
|
||||
} else if (status == FlowStatus.FAILURE_CHALLENGE) {
|
||||
AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator());
|
||||
processor.logFailure();
|
||||
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
|
||||
return sendChallenge(result, execution);
|
||||
} else if (status == AuthenticationProcessor.Status.ATTEMPTED) {
|
||||
} else if (status == FlowStatus.ATTEMPTED) {
|
||||
AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
|
||||
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
|
||||
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
|
||||
|
|
47
services/src/main/java/org/keycloak/authentication/FlowStatus.java
Executable file
47
services/src/main/java/org/keycloak/authentication/FlowStatus.java
Executable file
|
@ -0,0 +1,47 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
/**
|
||||
* Status of an execution/authenticator in a Authentication Flow
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public enum FlowStatus {
|
||||
/**
|
||||
* Successful execution
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* Execution offered a challenge. Optional executions will ignore this challenge. Alternative executions may
|
||||
* ignore the challenge depending on the status of other executions in the flow.
|
||||
*
|
||||
*/
|
||||
CHALLENGE,
|
||||
|
||||
/**
|
||||
* Irregardless of the execution's requirement, this challenge will be sent to the user.
|
||||
*
|
||||
*/
|
||||
FORCE_CHALLENGE,
|
||||
|
||||
/**
|
||||
* Flow will be aborted and a Response provided by the execution will be sent.
|
||||
*
|
||||
*/
|
||||
FAILURE_CHALLENGE,
|
||||
|
||||
/**
|
||||
* Flow will be aborted.
|
||||
*
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* This is not an error condition. Execution was attempted, but the authenticator is unable to process the request. An example of this is if
|
||||
* a Kerberos authenticator did not see a negotiate header. There was no error, but the execution was attempted.
|
||||
*
|
||||
*/
|
||||
ATTEMPTED
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
|
@ -18,6 +19,15 @@ import javax.ws.rs.core.UriInfo;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface RequiredActionContext {
|
||||
void ignore();
|
||||
|
||||
public static enum Status {
|
||||
CHALLENGE,
|
||||
SUCCESS,
|
||||
IGNORE,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
/**
|
||||
* Current event builder being used
|
||||
*
|
||||
|
@ -46,4 +56,10 @@ public interface RequiredActionContext {
|
|||
* @return
|
||||
*/
|
||||
String generateAccessCode(String action);
|
||||
|
||||
Status getStatus();
|
||||
|
||||
void challenge(Response response);
|
||||
void failure();
|
||||
void success();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package org.keycloak.authentication;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
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 javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class RequiredActionContextResult implements RequiredActionContext {
|
||||
protected UserSessionModel userSession;
|
||||
protected ClientSessionModel clientSession;
|
||||
protected RealmModel realm;
|
||||
protected EventBuilder eventBuilder;
|
||||
protected KeycloakSession session;
|
||||
protected Status status;
|
||||
protected Response challenge;
|
||||
protected HttpRequest httpRequest;
|
||||
protected UserModel user;
|
||||
|
||||
public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession,
|
||||
RealmModel realm, EventBuilder eventBuilder, KeycloakSession session,
|
||||
HttpRequest httpRequest,
|
||||
UserModel user) {
|
||||
this.userSession = userSession;
|
||||
this.clientSession = clientSession;
|
||||
this.realm = realm;
|
||||
this.eventBuilder = eventBuilder;
|
||||
this.session = session;
|
||||
this.httpRequest = httpRequest;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventBuilder getEvent() {
|
||||
return eventBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSessionModel getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel getUserSession() {
|
||||
return userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnection getConnection() {
|
||||
return session.getContext().getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriInfo getUriInfo() {
|
||||
return session.getContext().getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequest getHttpRequest() {
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateAccessCode(String action) {
|
||||
ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
|
||||
code.setAction(action);
|
||||
return code.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void challenge(Response response) {
|
||||
status = Status.CHALLENGE;
|
||||
challenge = response;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure() {
|
||||
status = Status.FAILURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success() {
|
||||
status = Status.SUCCESS;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ignore() {
|
||||
status = Status.IGNORE;
|
||||
}
|
||||
|
||||
public Response getChallenge() {
|
||||
return challenge;
|
||||
}
|
||||
}
|
|
@ -28,18 +28,14 @@ public interface RequiredActionProvider extends Provider {
|
|||
* @param context
|
||||
* @return
|
||||
*/
|
||||
Response requiredActionChallenge(RequiredActionContext context);
|
||||
void requiredActionChallenge(RequiredActionContext context);
|
||||
|
||||
/**
|
||||
* This is an optional method. If the required action has a more complex interaction, you can encapsulate it within
|
||||
* a REST service. This method returns a JAX-RS sub locator object that can be referenced at:
|
||||
*
|
||||
* /realms/{realm}/login-actions/required-actions/{provider-id}
|
||||
* Called when a required action has form input you want to process.
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
Object jaxrsService(RequiredActionContext context);
|
||||
void processAction(RequiredActionContext context);
|
||||
|
||||
/**
|
||||
* Provider id of this required action. Must match ProviderFactory.getId().
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
@ -15,11 +13,9 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -27,12 +23,11 @@ import java.util.List;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public abstract class AbstractFormAuthenticator implements Authenticator {
|
||||
public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuthenticator {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(AbstractFormAuthenticator.class);
|
||||
private static final Logger logger = Logger.getLogger(AbstractUsernameFormAuthenticator.class);
|
||||
|
||||
public static final String REGISTRATION_FORM_ACTION = "registration_form";
|
||||
public static final String EXECUTION = "execution";
|
||||
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";
|
||||
|
||||
@Override
|
||||
|
@ -40,31 +35,6 @@ public abstract class AbstractFormAuthenticator implements Authenticator {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
protected Response invalidUser(AuthenticationFlowContext context) {
|
||||
return loginForm(context)
|
||||
.setError(Messages.INVALID_USER)
|
||||
|
@ -129,7 +99,7 @@ public abstract class AbstractFormAuthenticator implements Authenticator {
|
|||
return false;
|
||||
}
|
||||
context.getEvent().detail(Details.USERNAME, username);
|
||||
context.getClientSession().setNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME, username);
|
||||
context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
|
||||
|
||||
UserModel user = null;
|
||||
try {
|
|
@ -22,7 +22,7 @@ import java.util.List;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator {
|
||||
public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
|
||||
public static final String TOTP_FORM_ACTION = "totp";
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,7 +25,7 @@ import java.util.Map;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Authenticator{
|
||||
public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator{
|
||||
public static final String KERBEROS_DISABLED = "kerberos_disabled";
|
||||
protected static Logger logger = Logger.getLogger(SpnegoAuthenticator.class);
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import javax.ws.rs.core.Response;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UsernamePasswordForm extends AbstractFormAuthenticator implements Authenticator {
|
||||
public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator implements Authenticator {
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.keycloak.authentication.authenticators.directgrant;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractFormAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
|
@ -40,7 +40,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
|
|||
return;
|
||||
}
|
||||
context.getEvent().detail(Details.USERNAME, username);
|
||||
context.getClientSession().setNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME, username);
|
||||
context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);
|
||||
|
||||
UserModel user = null;
|
||||
try {
|
||||
|
|
|
@ -4,56 +4,20 @@ import org.keycloak.Config;
|
|||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.freemarker.FreeMarkerException;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.authentication.AbstractFormRequiredAction;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
|
||||
public class TermsAndConditions extends AbstractFormRequiredAction implements RequiredActionFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "terms_and_conditions";
|
||||
|
||||
public static class Resource {
|
||||
|
||||
public Resource(RequiredActionContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
protected RequiredActionContext context;
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response agree(final MultivaluedMap<String, String> formData) throws URISyntaxException, IOException, FreeMarkerException {
|
||||
if (formData.containsKey("cancel")) {
|
||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
||||
protocol.setRealm(context.getRealm())
|
||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(context.getUriInfo());
|
||||
context.getEvent().error(Errors.REJECTED_BY_USER);
|
||||
return protocol.consentDenied(context.getClientSession());
|
||||
}
|
||||
context.getUser().removeRequiredAction(PROVIDER_ID);
|
||||
return AuthenticationManager.nextActionAfterAuthentication(context.getSession(), context.getUserSession(), context.getClientSession(), context.getConnection(), context.getHttpRequest(), context.getUriInfo(), context.getEvent());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequiredActionProvider create(KeycloakSession session) {
|
||||
return this;
|
||||
|
@ -87,17 +51,21 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
|
|||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Response requiredActionChallenge(RequiredActionContext context) {
|
||||
return context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(context.generateAccessCode(getProviderId()))
|
||||
.setUser(context.getUser())
|
||||
.createForm("terms.ftl", new HashMap<String, Object>());
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
Response challenge = form(context).createForm("terms.ftl");
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jaxrsService(RequiredActionContext context) {
|
||||
return new Resource(context);
|
||||
public void processAction(RequiredActionContext context) {
|
||||
if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) {
|
||||
context.failure();
|
||||
return;
|
||||
}
|
||||
context.success();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -105,8 +73,4 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
|
|||
return "Terms and Conditions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,21 +48,20 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response requiredActionChallenge(RequiredActionContext context) {
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
LoginFormsProvider loginFormsProvider = context.getSession()
|
||||
.getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(context.generateAccessCode(getProviderId()))
|
||||
.setUser(context.getUser());
|
||||
return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD);
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jaxrsService(RequiredActionContext context) {
|
||||
// this is handled by LoginActionsService at the moment
|
||||
return null;
|
||||
public void processAction(RequiredActionContext context) {
|
||||
context.failure();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
|
|
@ -23,18 +23,17 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response requiredActionChallenge(RequiredActionContext context) {
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(context.generateAccessCode(getProviderId()))
|
||||
.setUser(context.getUser());
|
||||
return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
|
||||
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jaxrsService(RequiredActionContext context) {
|
||||
// this is handled by LoginActionsService at the moment
|
||||
// todo should be refactored to contain it here
|
||||
return null;
|
||||
public void processAction(RequiredActionContext context) {
|
||||
context.failure();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -25,18 +25,17 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response requiredActionChallenge(RequiredActionContext context) {
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(context.generateAccessCode(getProviderId()))
|
||||
.setUser(context.getUser());
|
||||
return loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jaxrsService(RequiredActionContext context) {
|
||||
// this is handled by LoginActionsService at the moment
|
||||
// todo should be refactored to contain it here
|
||||
return null;
|
||||
public void processAction(RequiredActionContext context) {
|
||||
context.failure();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -34,9 +34,10 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
|||
}
|
||||
}
|
||||
@Override
|
||||
public Response requiredActionChallenge(RequiredActionContext context) {
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
if (Validation.isBlank(context.getUser().getEmail())) {
|
||||
return null;
|
||||
context.ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success();
|
||||
|
@ -45,13 +46,13 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
|||
LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(context.generateAccessCode(getProviderId()))
|
||||
.setUser(context.getUser());
|
||||
return loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||
Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||
context.challenge(challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object jaxrsService(RequiredActionContext context) {
|
||||
// this is handled by LoginActionsService at the moment
|
||||
return null;
|
||||
public void processAction(RequiredActionContext context) {
|
||||
context.failure();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,10 +7,13 @@ import org.keycloak.ClientConnection;
|
|||
import org.keycloak.RSATokenVerifier;
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionContextResult;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
|
@ -418,8 +421,9 @@ public class AuthenticationManager {
|
|||
final UserModel user = userSession.getUser();
|
||||
final ClientModel client = clientSession.getClient();
|
||||
|
||||
RequiredActionContext context = evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
|
||||
evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user);
|
||||
|
||||
RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user);
|
||||
|
||||
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
|
||||
|
||||
|
@ -429,11 +433,23 @@ public class AuthenticationManager {
|
|||
for (String action : requiredActions) {
|
||||
RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action);
|
||||
RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
|
||||
Response challenge = actionProvider.requiredActionChallenge(context);
|
||||
if (challenge != null) {
|
||||
return challenge;
|
||||
}
|
||||
actionProvider.requiredActionChallenge(context);
|
||||
|
||||
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
||||
protocol.setRealm(context.getRealm())
|
||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(context.getUriInfo());
|
||||
event.error(Errors.REJECTED_BY_USER);
|
||||
return protocol.consentDenied(context.getClientSession());
|
||||
}
|
||||
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
if (client.isConsentRequired()) {
|
||||
|
||||
|
@ -484,58 +500,26 @@ public class AuthenticationManager {
|
|||
|
||||
}
|
||||
|
||||
public static RequiredActionContext 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) {
|
||||
RequiredActionContext context = new RequiredActionContext() {
|
||||
public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) {
|
||||
RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user) {
|
||||
@Override
|
||||
public EventBuilder getEvent() {
|
||||
return event;
|
||||
public void challenge(Response response) {
|
||||
throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
public void failure() {
|
||||
throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
public void success() {
|
||||
throw new RuntimeException("Not allowed to call success() within evaluateTriggers()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSessionModel getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel getUserSession() {
|
||||
return userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnection getConnection() {
|
||||
return clientConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriInfo getUriInfo() {
|
||||
return uriInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequest getHttpRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateAccessCode(String action) {
|
||||
ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
|
||||
code.setAction(action);
|
||||
return code.getCode();
|
||||
public void ignore() {
|
||||
throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()");
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -543,9 +527,8 @@ public class AuthenticationManager {
|
|||
for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) {
|
||||
if (!model.isEnabled()) continue;
|
||||
RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId());
|
||||
provider.evaluateTriggers(context);
|
||||
provider.evaluateTriggers(result);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.jboss.resteasy.spi.HttpRequest;
|
|||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionContextResult;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailProvider;
|
||||
|
@ -125,6 +126,10 @@ public class LoginActionsService {
|
|||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "authenticateForm");
|
||||
}
|
||||
|
||||
public static UriBuilder requiredActionProcessor(UriInfo uriInfo) {
|
||||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "requiredActionPOST");
|
||||
}
|
||||
|
||||
public static UriBuilder registrationFormProcessor(UriInfo uriInfo) {
|
||||
return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister");
|
||||
}
|
||||
|
@ -829,10 +834,26 @@ public class LoginActionsService {
|
|||
}
|
||||
}
|
||||
|
||||
@Path("required-actions/{action}")
|
||||
public Object requiredAction(@QueryParam("code") final String code,
|
||||
@PathParam("action") String action) {
|
||||
event.event(EventType.LOGIN);
|
||||
@Path("required-action")
|
||||
@POST
|
||||
public Response requiredActionPOST(@QueryParam("code") final String code,
|
||||
@QueryParam("action") String action) {
|
||||
return processRequireAction(code, action);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Path("required-action")
|
||||
@GET
|
||||
public Response requiredActionGET(@QueryParam("code") final String code,
|
||||
@QueryParam("action") String action) {
|
||||
return processRequireAction(code, action);
|
||||
}
|
||||
|
||||
public Response processRequireAction(final String code, String action) {
|
||||
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
||||
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
|
||||
if (action == null) {
|
||||
logger.error("required action query param was null");
|
||||
event.error(Errors.INVALID_CODE);
|
||||
|
@ -859,54 +880,11 @@ public class LoginActionsService {
|
|||
throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
|
||||
}
|
||||
|
||||
initEvent(clientSession);
|
||||
|
||||
RequiredActionContext context = new RequiredActionContext() {
|
||||
@Override
|
||||
public EventBuilder getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return getUserSession().getUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSessionModel getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel getUserSession() {
|
||||
return clientSession.getUserSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnection getConnection() {
|
||||
return clientConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriInfo getUriInfo() {
|
||||
return uriInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequest getHttpRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
@Override
|
||||
RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser()) {
|
||||
@Override
|
||||
public String generateAccessCode(String action) {
|
||||
String clientSessionAction = clientSession.getAction();
|
||||
if (action.equals(clientSessionAction)) {
|
||||
|
@ -917,10 +895,32 @@ public class LoginActionsService {
|
|||
code.setAction(action);
|
||||
return code.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ignore() {
|
||||
throw new RuntimeException("Cannot call ignore within processAction()");
|
||||
}
|
||||
};
|
||||
return provider.jaxrsService(context);
|
||||
|
||||
provider.processAction(context);
|
||||
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());
|
||||
return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
|
||||
}
|
||||
if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
|
||||
return context.getChallenge();
|
||||
}
|
||||
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
|
||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
||||
protocol.setRealm(context.getRealm())
|
||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
||||
.setUriInfo(context.getUriInfo());
|
||||
event.detail(Details.CUSTOM_REQUIRED_ACTION, action).error(Errors.REJECTED_BY_USER);
|
||||
return protocol.consentDenied(context.getClientSession());
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unreachable");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* JBoss, Home of Professional Open Source.
|
||||
* Copyright 2012, Red Hat, Inc., and individual contributors
|
||||
* as indicated by the @author tags. See the copyright.txt file in the
|
||||
* distribution for a full listing of individual contributors.
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this software; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
*/
|
||||
package org.keycloak.testsuite.actions;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.requiredactions.TermsAndConditions;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||
import org.keycloak.testsuite.pages.TermsAndConditionsPage;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class TermsAndConditionsTest {
|
||||
|
||||
@ClassRule
|
||||
public static KeycloakRule keycloakRule = new KeycloakRule();
|
||||
|
||||
@Rule
|
||||
public WebRule webRule = new WebRule(this);
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(keycloakRule);
|
||||
|
||||
@WebResource
|
||||
protected WebDriver driver;
|
||||
|
||||
@WebResource
|
||||
protected AppPage appPage;
|
||||
|
||||
@WebResource
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@WebResource
|
||||
protected TermsAndConditionsPage termsPage;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
|
||||
@Override
|
||||
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
|
||||
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
|
||||
user.addRequiredAction(TermsAndConditions.PROVIDER_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void termsAccepted() {
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
termsPage.assertCurrent();
|
||||
|
||||
termsPage.acceptTerms();
|
||||
|
||||
String sessionId = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent().getSessionId();
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
||||
events.expectLogin().session(sessionId).assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void termsDeclined() {
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
|
||||
termsPage.assertCurrent();
|
||||
|
||||
termsPage.declineTerms();
|
||||
|
||||
events.expectLogin().detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID)
|
||||
.error(Errors.REJECTED_BY_USER)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* JBoss, Home of Professional Open Source.
|
||||
* Copyright 2012, Red Hat, Inc., and individual contributors
|
||||
* as indicated by the @author tags. See the copyright.txt file in the
|
||||
* distribution for a full listing of individual contributors.
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as
|
||||
* published by the Free Software Foundation; either version 2.1 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this software; if not, write to the Free
|
||||
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
|
||||
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
|
||||
*/
|
||||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class TermsAndConditionsPage extends AbstractPage {
|
||||
|
||||
@FindBy(id = "kc-accept")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(id = "kc-decline")
|
||||
private WebElement cancelButton;
|
||||
|
||||
public boolean isCurrent() {
|
||||
return driver.getTitle().equals("Terms and Conditions");
|
||||
}
|
||||
|
||||
public void acceptTerms() {
|
||||
submitButton.click();
|
||||
}
|
||||
public void declineTerms() {
|
||||
cancelButton.click();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue