KEYCLOAK-16401 Deny/Allow access in a conditional context
This commit is contained in:
parent
cd342ad571
commit
5a9068e732
15 changed files with 794 additions and 36 deletions
|
@ -44,5 +44,7 @@ public enum AuthenticationFlowError {
|
|||
IDENTITY_PROVIDER_NOT_FOUND,
|
||||
IDENTITY_PROVIDER_DISABLED,
|
||||
IDENTITY_PROVIDER_ERROR,
|
||||
DISPLAY_NOT_SUPPORTED
|
||||
DISPLAY_NOT_SUPPORTED,
|
||||
|
||||
ACCESS_DENIED
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.access;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* Authenticator will always successfully authenticate.
|
||||
* Useful for example in the conditional flows to be used after satisfying the previous conditions.
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class AllowAccessAuthenticator implements Authenticator {
|
||||
private final Logger log = Logger.getLogger(AllowAccessAuthenticator.class);
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
log.trace("Explicitly allowed access to the resource.");
|
||||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.access;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class AllowAccessAuthenticatorFactory implements AuthenticatorFactory {
|
||||
private final static AllowAccessAuthenticator SINGLETON = new AllowAccessAuthenticator();
|
||||
public static final String PROVIDER_ID = "allow-access-authenticator";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Allow access";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Authenticator will always successfully authenticate. Useful for example in the conditional flows to be used after satisfying the previous conditions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.access;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Explicitly deny access to the resources.
|
||||
* Useful for example in the conditional flows to be used after satisfying the previous conditions. after satisfying conditions in the conditional flow.
|
||||
*
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class DenyAccessAuthenticator implements Authenticator {
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
String errorMessage = Optional.ofNullable(context.getAuthenticatorConfig())
|
||||
.map(AuthenticatorConfigModel::getConfig)
|
||||
.map(f -> f.get(DenyAccessAuthenticatorFactory.ERROR_MESSAGE))
|
||||
.orElse(Messages.ACCESS_DENIED);
|
||||
|
||||
context.getEvent().error(Errors.ACCESS_DENIED);
|
||||
Response challenge = context.form()
|
||||
.setError(errorMessage)
|
||||
.createErrorPage(Response.Status.UNAUTHORIZED);
|
||||
context.failure(AuthenticationFlowError.ACCESS_DENIED, challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.access;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
public class DenyAccessAuthenticatorFactory implements AuthenticatorFactory {
|
||||
private static final DenyAccessAuthenticator SINGLETON = new DenyAccessAuthenticator();
|
||||
public static final String PROVIDER_ID = "deny-access-authenticator";
|
||||
|
||||
public static final String ERROR_MESSAGE = "denyErrorMessage";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Deny access";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Access will be always denied. Useful for example in the conditional flows to be used after satisfying the previous conditions";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
ProviderConfigProperty errorMessage = new ProviderConfigProperty();
|
||||
errorMessage.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
errorMessage.setName(ERROR_MESSAGE);
|
||||
errorMessage.setLabel("Error message");
|
||||
errorMessage.setHelpText("Error message which will be shown to the user. " +
|
||||
"You can directly define particular message or property, which will be used for mapping the error message f.e `deny-access-role1`. " +
|
||||
"If the field is blank, default property 'access-denied' is used.");
|
||||
return Collections.singletonList(errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
|
@ -20,12 +20,14 @@ public class ConditionalRoleAuthenticator implements ConditionalAuthenticator {
|
|||
AuthenticatorConfigModel authConfig = context.getAuthenticatorConfig();
|
||||
if (user != null && authConfig!=null && authConfig.getConfig()!=null) {
|
||||
String requiredRole = authConfig.getConfig().get(ConditionalRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE);
|
||||
boolean negateOutput = Boolean.parseBoolean(authConfig.getConfig().get(ConditionalRoleAuthenticatorFactory.CONF_NEGATE));
|
||||
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, requiredRole);
|
||||
if (role == null) {
|
||||
logger.errorv("Invalid role name submitted: {0}", requiredRole);
|
||||
return false;
|
||||
}
|
||||
return user.hasRole(role);
|
||||
|
||||
return negateOutput != user.hasRole(role);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -5,25 +5,15 @@ import org.keycloak.models.AuthenticationExecutionModel;
|
|||
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ConditionalRoleAuthenticatorFactory implements ConditionalAuthenticatorFactory {
|
||||
public static final String PROVIDER_ID = "conditional-user-role";
|
||||
protected static final String CONDITIONAL_USER_ROLE = "condUserRole";
|
||||
|
||||
private static List<ProviderConfigProperty> commonConfig;
|
||||
|
||||
static {
|
||||
commonConfig = Collections.unmodifiableList(ProviderConfigurationBuilder.create()
|
||||
.property().name(CONDITIONAL_USER_ROLE).label("User role")
|
||||
.helpText("Role the user should have to execute this flow. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference a client role the syntax is clientname.clientrole, i.e. myclient.myrole")
|
||||
.type(ProviderConfigProperty.ROLE_TYPE).add()
|
||||
.build()
|
||||
);
|
||||
}
|
||||
public static final String CONDITIONAL_USER_ROLE = "condUserRole";
|
||||
public static final String CONF_NEGATE = "negate";
|
||||
|
||||
@Override
|
||||
public void init(Scope config) {
|
||||
|
@ -61,7 +51,8 @@ public class ConditionalRoleAuthenticatorFactory implements ConditionalAuthentic
|
|||
}
|
||||
|
||||
private static final Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED,
|
||||
AuthenticationExecutionModel.Requirement.DISABLED
|
||||
};
|
||||
|
||||
@Override
|
||||
|
@ -81,7 +72,19 @@ public class ConditionalRoleAuthenticatorFactory implements ConditionalAuthentic
|
|||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return commonConfig;
|
||||
ProviderConfigProperty role = new ProviderConfigProperty();
|
||||
role.setType(ProviderConfigProperty.ROLE_TYPE);
|
||||
role.setName(CONDITIONAL_USER_ROLE);
|
||||
role.setLabel("User role");
|
||||
role.setHelpText("Role the user should have to execute this flow. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference a client role the syntax is clientname.clientrole, i.e. myclient.myrole");
|
||||
|
||||
ProviderConfigProperty negateOutput = new ProviderConfigProperty();
|
||||
negateOutput.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||
negateOutput.setName(CONF_NEGATE);
|
||||
negateOutput.setLabel("Negate output");
|
||||
negateOutput.setHelpText("Apply a NOT to the check result. When this is true, then the condition will evaluate to true just if user does NOT have the specified role. When this is false, the condition will evaluate to true just if user has the specified role");
|
||||
|
||||
return Arrays.asList(role, negateOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,7 +25,7 @@ public class ConditionalUserConfiguredAuthenticator implements ConditionalAuthen
|
|||
List<AuthenticationExecutionModel> alternativeExecutions = new LinkedList<>();
|
||||
context.getRealm().getAuthenticationExecutionsStream(flowId)
|
||||
//Check if the execution's authenticator is a conditional authenticator, as they must not be evaluated here.
|
||||
.filter(e -> isConditionalExecution(context, e))
|
||||
.filter(e -> !isConditionalExecution(context, e))
|
||||
.filter(e -> !Objects.equals(context.getExecution().getId(), e.getId()) && !e.isAuthenticatorFlow())
|
||||
.forEachOrdered(e -> {
|
||||
if (e.isRequired()) {
|
||||
|
@ -47,11 +47,9 @@ public class ConditionalUserConfiguredAuthenticator implements ConditionalAuthen
|
|||
.getProviderFactory(Authenticator.class, e.getAuthenticator());
|
||||
if (factory != null) {
|
||||
Authenticator auth = factory.create(context.getSession());
|
||||
if (auth instanceof ConditionalAuthenticator) {
|
||||
return false;
|
||||
}
|
||||
return (auth instanceof ConditionalAuthenticator);
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isConfiguredFor(AuthenticationExecutionModel model, AuthenticationFlowContext context) {
|
||||
|
|
|
@ -259,6 +259,9 @@ public class Messages {
|
|||
public static final String WEBAUTHN_ERROR_REGISTER_VERIFICATION = "webauthn-error-register-verification";
|
||||
public static final String WEBAUTHN_ERROR_USER_NOT_FOUND = "webauthn-error-user-not-found";
|
||||
|
||||
// Conditions in Conditional Flow
|
||||
public static final String ACCESS_DENIED = "access-denied";
|
||||
|
||||
public static final String DELETE_ACCOUNT_LACK_PRIVILEDGES = "deletingAccountForbidden";
|
||||
public static final String DELETE_ACCOUNT_ERROR = "errorDeletingAccount";
|
||||
|
||||
|
|
|
@ -49,4 +49,6 @@ org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFacto
|
|||
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory
|
|
@ -8,7 +8,6 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -19,8 +18,6 @@ public class ConditionalUserAttributeValue implements ConditionalAuthenticator {
|
|||
|
||||
@Override
|
||||
public boolean matchCondition(AuthenticationFlowContext context) {
|
||||
boolean result = false;
|
||||
|
||||
// Retrieve configuration
|
||||
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
||||
String attributeName = config.get(ConditionalUserAttributeValueFactory.CONF_ATTRIBUTE_NAME);
|
||||
|
@ -32,12 +29,8 @@ public class ConditionalUserAttributeValue implements ConditionalAuthenticator {
|
|||
throw new AuthenticationFlowException("authenticator: " + ConditionalUserAttributeValueFactory.PROVIDER_ID, AuthenticationFlowError.UNKNOWN_USER);
|
||||
}
|
||||
|
||||
result = user.getAttributeStream(attributeName).anyMatch(attr -> Objects.equals(attr, attributeValue));
|
||||
if (negateOutput) {
|
||||
result = !result;
|
||||
}
|
||||
|
||||
return result;
|
||||
boolean result = user.getAttributeStream(attributeName).anyMatch(attr -> Objects.equals(attr, attributeValue));
|
||||
return negateOutput != result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -216,6 +216,12 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"Set a user attribute");
|
||||
addProviderInfo(result, "idp-detect-existing-broker-user", "Detect existing broker user",
|
||||
"Detect if there is an existing Keycloak account with same email like identity provider. If no, throw an error.");
|
||||
|
||||
addProviderInfo(result, "deny-access-authenticator", "Deny access",
|
||||
"Access will be always denied. Useful for example in the conditional flows to be used after satisfying the previous conditions");
|
||||
addProviderInfo(result, "allow-access-authenticator", "Allow access",
|
||||
"Authenticator will always successfully authenticate. Useful for example in the conditional flows to be used after satisfying the previous conditions");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,382 @@
|
|||
package org.keycloak.testsuite.forms;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
import org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.authentication.ConditionalUserAttributeValueFactory;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
import static org.keycloak.testsuite.forms.BrowserFlowTest.revertFlows;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
|
||||
*/
|
||||
@AuthServerContainerExclude(REMOTE)
|
||||
public class AllowDenyAuthenticatorTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Page
|
||||
protected LoginUsernameOnlyPage loginUsernameOnlyPage;
|
||||
|
||||
@Page
|
||||
protected PasswordPage passwordPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenyAccessWithDefaultMessage() {
|
||||
testErrorMessageInDenyAccess(null, "Access denied");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenyAccessWithParticularMessage() {
|
||||
final String message = "You are not allowed to authenticate.";
|
||||
testErrorMessageInDenyAccess(message, message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenyAccessWithProperty() {
|
||||
final String property = "brokerLinkingSessionExpired";
|
||||
final String message = "Requested broker account linking, but current session is no longer valid.";
|
||||
|
||||
testErrorMessageInDenyAccess(property, message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDenyAccessWithNotExistingProperty() {
|
||||
final String property = "not-existing-property";
|
||||
final String message = "not-existing-property";
|
||||
|
||||
testErrorMessageInDenyAccess(property, message);
|
||||
}
|
||||
|
||||
/* Helper method for error messaged in Deny Authenticator */
|
||||
private void testErrorMessageInDenyAccess(String setUpMessage, String expectedMessage) {
|
||||
final String flowAlias = "browser - deny defaultMessage";
|
||||
final String userWithoutAttribute = "test-user@localhost";
|
||||
|
||||
Map<String, String> denyAccessConfigMap = new HashMap<>();
|
||||
if (setUpMessage != null) {
|
||||
denyAccessConfigMap.put(DenyAccessAuthenticatorFactory.ERROR_MESSAGE, setUpMessage);
|
||||
}
|
||||
|
||||
configureBrowserFlowWithDenyAccess(flowAlias, denyAccessConfigMap);
|
||||
|
||||
try {
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userWithoutAttribute);
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertThat(errorPage.getError(), is(expectedMessage));
|
||||
|
||||
events.expectLogin()
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.error(Errors.ACCESS_DENIED)
|
||||
.detail(Details.USERNAME, userWithoutAttribute)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
revertFlows(testRealm(), flowAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test checks that if user does not have specific attribute, then the access is denied.
|
||||
*/
|
||||
@Test
|
||||
public void testDenyAccessWithNegateUserAttributeCondition() {
|
||||
final String flowAlias = "browser - user attribute condition";
|
||||
final String userWithoutAttribute = "test-user@localhost";
|
||||
final String errorMessage = "You don't have necessary attribute.";
|
||||
|
||||
Map<String, String> attributeConfigMap = new HashMap<>();
|
||||
attributeConfigMap.put(ConditionalUserAttributeValueFactory.CONF_ATTRIBUTE_NAME, "attribute");
|
||||
attributeConfigMap.put(ConditionalUserAttributeValueFactory.CONF_ATTRIBUTE_EXPECTED_VALUE, "value");
|
||||
attributeConfigMap.put(ConditionalUserAttributeValueFactory.CONF_NOT, "true");
|
||||
|
||||
Map<String, String> denyAccessConfigMap = new HashMap<>();
|
||||
denyAccessConfigMap.put(DenyAccessAuthenticatorFactory.ERROR_MESSAGE, errorMessage);
|
||||
|
||||
configureBrowserFlowWithDenyAccessInConditionalFlow(flowAlias, ConditionalUserAttributeValueFactory.PROVIDER_ID, attributeConfigMap, denyAccessConfigMap);
|
||||
|
||||
try {
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userWithoutAttribute);
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertThat(errorPage.getError(), is(errorMessage));
|
||||
|
||||
events.expectLogin()
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.error(Errors.ACCESS_DENIED)
|
||||
.detail(Details.USERNAME, userWithoutAttribute)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
revertFlows(testRealm(), flowAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny access, if user has defined the role and print error message.
|
||||
*/
|
||||
@Test
|
||||
public void testDenyAccessWithRoleCondition() {
|
||||
denyAccessWithRoleCondition(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny access, if user has NOT defined the role and print error message.
|
||||
*/
|
||||
@Test
|
||||
public void testDenyAccessWithNegateRoleCondition() {
|
||||
denyAccessWithRoleCondition(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for deny access with role condition
|
||||
*
|
||||
* @param negateOutput
|
||||
*/
|
||||
private void denyAccessWithRoleCondition(boolean negateOutput) {
|
||||
final String flowAlias = "browser-deny";
|
||||
final String userWithRole = "test-user@localhost";
|
||||
final String userWithoutRole = "john-doh@localhost";
|
||||
final String role = "offline_access";
|
||||
final String errorMessage = "Your account doesn't have the required role";
|
||||
|
||||
Map<String, String> config = new HashMap<>();
|
||||
config.put(ConditionalRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE, role);
|
||||
config.put(ConditionalRoleAuthenticatorFactory.CONF_NEGATE, Boolean.toString(negateOutput));
|
||||
|
||||
Map<String, String> denyConfig = new HashMap<>();
|
||||
denyConfig.put(DenyAccessAuthenticatorFactory.ERROR_MESSAGE, errorMessage);
|
||||
|
||||
configureBrowserFlowWithDenyAccessInConditionalFlow(flowAlias, ConditionalRoleAuthenticatorFactory.PROVIDER_ID, config, denyConfig);
|
||||
|
||||
denyAccessInConditionalFlow(flowAlias,
|
||||
negateOutput ? userWithoutRole : userWithRole,
|
||||
negateOutput ? userWithRole : userWithoutRole,
|
||||
errorMessage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for deny access with two opposites cases
|
||||
*/
|
||||
private void denyAccessInConditionalFlow(String flowAlias, String userCondMatch, String userCondNotMatch, String errorMessage) {
|
||||
try {
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userCondMatch);
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertThat(errorPage.getError(), is(errorMessage));
|
||||
|
||||
events.expectLogin()
|
||||
.user((String) null)
|
||||
.session((String) null)
|
||||
.error(Errors.ACCESS_DENIED)
|
||||
.detail(Details.USERNAME, userCondMatch)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
|
||||
final String userCondNotMatchId = testRealm().users().search(userCondNotMatch).get(0).getId();
|
||||
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userCondNotMatch);
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login("password");
|
||||
|
||||
events.expectLogin().user(userCondNotMatchId)
|
||||
.detail(Details.USERNAME, userCondNotMatch)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
revertFlows(testRealm(), flowAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test checks that if user has NOT the required role, the user has to enter the password
|
||||
*/
|
||||
@Test
|
||||
public void testSkipExecutionUserHasNotRoleCondition() {
|
||||
final String userWithoutRole = "john-doh@localhost";
|
||||
final String role = "offline_access";
|
||||
final String newFlowAlias = "browser - allow skip";
|
||||
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
configMap.put(ConditionalRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE, role);
|
||||
configMap.put(ConditionalRoleAuthenticatorFactory.CONF_NEGATE, "false");
|
||||
|
||||
configureBrowserFlowWithSkipExecutionInConditionalFlow(newFlowAlias, ConditionalRoleAuthenticatorFactory.PROVIDER_ID, configMap);
|
||||
try {
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userWithoutRole);
|
||||
|
||||
final String testUserWithoutRoleId = testRealm().users().search(userWithoutRole).get(0).getId();
|
||||
|
||||
passwordPage.assertCurrent();
|
||||
passwordPage.login("password");
|
||||
|
||||
events.expectLogin()
|
||||
.user(testUserWithoutRoleId)
|
||||
.detail(Details.USERNAME, userWithoutRole)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
revertFlows(testRealm(), newFlowAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This test checks that if user has the required role, the user skips the other executions
|
||||
*/
|
||||
@Test
|
||||
public void testSkipOtherExecutionsIfUserHasRoleCondition() {
|
||||
final String userWithRole = "test-user@localhost";
|
||||
final String role = "offline_access";
|
||||
final String newFlowAlias = "browser - allow skip";
|
||||
|
||||
Map<String, String> configMap = new HashMap<>();
|
||||
configMap.put(ConditionalRoleAuthenticatorFactory.CONDITIONAL_USER_ROLE, role);
|
||||
configMap.put(ConditionalRoleAuthenticatorFactory.CONF_NEGATE, "false");
|
||||
|
||||
configureBrowserFlowWithSkipExecutionInConditionalFlow(newFlowAlias, ConditionalRoleAuthenticatorFactory.PROVIDER_ID, configMap);
|
||||
try {
|
||||
loginUsernameOnlyPage.open();
|
||||
loginUsernameOnlyPage.assertCurrent();
|
||||
loginUsernameOnlyPage.login(userWithRole);
|
||||
|
||||
final String testUserWithRoleId = testRealm().users().search(userWithRole).get(0).getId();
|
||||
|
||||
events.expectLogin()
|
||||
.user(testUserWithRoleId)
|
||||
.detail(Details.USERNAME, userWithRole)
|
||||
.removeDetail(Details.CONSENT)
|
||||
.assertEvent();
|
||||
} finally {
|
||||
revertFlows(testRealm(), newFlowAlias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow contains:
|
||||
* UsernameForm REQUIRED
|
||||
* Subflow CONDITIONAL
|
||||
* ** condition
|
||||
* ** Deny Access REQUIRED
|
||||
* Password REQUIRED
|
||||
*
|
||||
* @param newFlowAlias
|
||||
* @param conditionProviderId
|
||||
* @param conditionConfig
|
||||
* @param denyConfig
|
||||
*/
|
||||
private void configureBrowserFlowWithDenyAccessInConditionalFlow(String newFlowAlias, String conditionProviderId, Map<String, String> conditionConfig, Map<String, String> denyConfig) {
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.CONDITIONAL, subflow -> subflow
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, conditionProviderId, config -> config.setConfig(conditionConfig))
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, DenyAccessAuthenticatorFactory.PROVIDER_ID, config -> config.setConfig(denyConfig))
|
||||
)
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID)
|
||||
)
|
||||
.defineAsBrowserFlow() // Activate this new flow
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow contains:
|
||||
* UsernameForm REQUIRED
|
||||
* Deny Access REQUIRED
|
||||
*
|
||||
* @param newFlowAlias
|
||||
* @param denyConfig
|
||||
*/
|
||||
private void configureBrowserFlowWithDenyAccess(String newFlowAlias, Map<String, String> denyConfig) {
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, DenyAccessAuthenticatorFactory.PROVIDER_ID, config -> config.setConfig(denyConfig))
|
||||
)
|
||||
.defineAsBrowserFlow() // Activate this new flow
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This flow contains:
|
||||
* UsernameForm REQUIRED
|
||||
* Subflow REQUIRED
|
||||
* ** subflow ALTERNATIVE
|
||||
* *** conditional-subflow CONDITIONAL
|
||||
* **** condition REQUIRED
|
||||
* **** Allow Access REQUIRED
|
||||
* ** Password ALTERNATIVE
|
||||
*
|
||||
* @param newFlowAlias
|
||||
* @param conditionProviderId
|
||||
* @param configMap
|
||||
*/
|
||||
private void configureBrowserFlowWithSkipExecutionInConditionalFlow(String newFlowAlias, String conditionProviderId, Map<String, String> configMap) {
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.inForms(forms -> forms
|
||||
.clear()
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID)
|
||||
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.REQUIRED, subflow -> subflow
|
||||
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.ALTERNATIVE, flow -> flow
|
||||
.addSubFlowExecution(AuthenticationExecutionModel.Requirement.CONDITIONAL, condFlow -> condFlow
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, conditionProviderId, config -> config.setConfig(configMap))
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, AllowAccessAuthenticatorFactory.PROVIDER_ID)))
|
||||
.addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.ALTERNATIVE, PasswordFormFactory.PROVIDER_ID)
|
||||
|
||||
))
|
||||
.defineAsBrowserFlow() // Activate this new flow
|
||||
);
|
||||
}
|
||||
}
|
|
@ -24,7 +24,13 @@ import org.keycloak.models.AuthenticationExecutionModel.Requirement;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.*;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.ActionURIUtils;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
|
@ -40,7 +46,10 @@ import org.keycloak.testsuite.pages.LoginPage;
|
|||
import org.keycloak.testsuite.pages.LoginTotpPage;
|
||||
import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
|
||||
import org.keycloak.testsuite.pages.PasswordPage;
|
||||
import org.keycloak.testsuite.util.*;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.URLUtils;
|
||||
import org.keycloak.testsuite.authentication.ConditionalUserAttributeValueFactory;
|
||||
import org.keycloak.testsuite.authentication.SetUserAttributeAuthenticatorFactory;
|
||||
import org.openqa.selenium.By;
|
||||
|
@ -52,8 +61,8 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||
import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
|
||||
|
@ -1306,7 +1315,7 @@ public class BrowserFlowTest extends AbstractTestRealmKeycloakTest {
|
|||
revertFlows(testRealm(), flowToDeleteAlias);
|
||||
}
|
||||
|
||||
static void revertFlows(RealmResource realmResource, String flowToDeleteAlias) {
|
||||
public static void revertFlows(RealmResource realmResource, String flowToDeleteAlias) {
|
||||
List<AuthenticationFlowRepresentation> flows = realmResource.flows().getFlows();
|
||||
|
||||
// Set default browser flow
|
||||
|
|
|
@ -411,3 +411,4 @@ loggingOutImmediately=Logging you out immediately
|
|||
accountUnusable=Any subsequent use of the application will not be possible with this account
|
||||
userDeletedSuccessfully=User deleted successfully
|
||||
|
||||
access-denied=Access denied
|
Loading…
Reference in a new issue