From 8f9ed32a66c970bf4d77b53d92446665ea813d2f Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Fri, 23 Jun 2017 15:16:23 +0200 Subject: [PATCH 1/3] KEYCLOAK-5078 ConcurrencyTest fails intermittently This commit fixes 401 Unauthorized issues --- .../models/cache/infinispan/GroupAdapter.java | 4 ++-- .../models/cache/infinispan/RealmAdapter.java | 4 ++-- .../models/cache/infinispan/UserAdapter.java | 2 +- .../keycloak/credential/CredentialModel.java | 17 ++++++++++++++++- .../keycloak/models/cache/CachedUserModel.java | 4 ++-- .../credential/PasswordCredentialProvider.java | 12 ++++++++---- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java index d32d35cc36..21bcc66ed6 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java @@ -35,7 +35,7 @@ import java.util.Set; * @version $Revision: 1 $ */ public class GroupAdapter implements GroupModel { - protected GroupModel updated; + protected volatile GroupModel updated; protected CachedGroup cached; protected RealmCacheSession cacheSession; protected KeycloakSession keycloakSession; @@ -56,7 +56,7 @@ public class GroupAdapter implements GroupModel { } } - protected boolean invalidated; + protected volatile boolean invalidated; public void invalidate() { invalidated = true; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 0bed8262a1..d1945ad42a 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -55,7 +55,7 @@ import java.util.concurrent.ConcurrentHashMap; public class RealmAdapter implements CachedRealmModel { protected CachedRealm cached; protected RealmCacheSession cacheSession; - protected RealmModel updated; + protected volatile RealmModel updated; protected RealmCache cache; protected KeycloakSession session; @@ -75,7 +75,7 @@ public class RealmAdapter implements CachedRealmModel { return updated; } - protected boolean invalidated; + protected volatile boolean invalidated; protected void invalidateFlag() { invalidated = true; diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 3094521fd4..0056edf493 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -41,7 +41,7 @@ import java.util.concurrent.ConcurrentHashMap; * @version $Revision: 1 $ */ public class UserAdapter implements CachedUserModel { - protected UserModel updated; + protected volatile UserModel updated; protected CachedUser cached; protected UserCacheSession userProviderCache; protected KeycloakSession keycloakSession; diff --git a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java index d3600ba125..f466183508 100755 --- a/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java +++ b/server-spi/src/main/java/org/keycloak/credential/CredentialModel.java @@ -56,7 +56,22 @@ public class CredentialModel implements Serializable { private int period; private MultivaluedHashMap config; - + public CredentialModel shallowClone() { + CredentialModel res = new CredentialModel(); + res.id = id; + res.type = type; + res.value = value; + res.device = device; + res.salt = salt; + res.hashIterations = hashIterations; + res.createdDate = createdDate; + res.counter = counter; + res.algorithm = algorithm; + res.digits = digits; + res.period = period; + res.config = config; + return res; + } public String getId() { return id; diff --git a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java index 8434d9de4c..1d3f59f289 100644 --- a/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/cache/CachedUserModel.java @@ -18,7 +18,7 @@ package org.keycloak.models.cache; import org.keycloak.models.UserModel; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Cached users will implement this interface @@ -55,5 +55,5 @@ public interface CachedUserModel extends UserModel { * * @return */ - ConcurrentHashMap getCachedWith(); + ConcurrentMap getCachedWith(); } diff --git a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java index a3f468ed74..2be347130a 100644 --- a/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java +++ b/services/src/main/java/org/keycloak/credential/PasswordCredentialProvider.java @@ -56,12 +56,14 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia } public CredentialModel getPassword(RealmModel realm, UserModel user) { - List passwords; + List passwords = null; if (user instanceof CachedUserModel && !((CachedUserModel)user).isMarkedForEviction()) { CachedUserModel cached = (CachedUserModel)user; passwords = (List)cached.getCachedWith().get(PASSWORD_CACHE_KEY); - } else { + } + // if the model was marked for eviction while passwords were initialized, override it from credentialStore + if (! (user instanceof CachedUserModel) || ((CachedUserModel) user).isMarkedForEviction()) { passwords = getCredentialStore().getStoredCredentialsByType(realm, user, CredentialModel.PASSWORD); } if (passwords == null || passwords.isEmpty()) return null; @@ -207,8 +209,10 @@ public class PasswordCredentialProvider implements CredentialProvider, Credentia return true; } - hash.encode(cred.getValue(), policy.getHashIterations(), password); - getCredentialStore().updateCredential(realm, user, password); + CredentialModel newPassword = password.shallowClone(); + hash.encode(cred.getValue(), policy.getHashIterations(), newPassword); + getCredentialStore().updateCredential(realm, user, newPassword); + UserCache userCache = session.userCache(); if (userCache != null) { userCache.evict(realm, user); From 756d996a4adaeba369ca8487e7dbb3fb880dcfc9 Mon Sep 17 00:00:00 2001 From: mposolda Date: Thu, 22 Jun 2017 22:23:20 +0200 Subject: [PATCH 2/3] KEYCLOAK-5085 RHSSO-1027 Fix to handle the exception thrown from alternative flow --- .../DefaultAuthenticationFlow.java | 17 +- .../ExpectedParamAuthenticator.java | 87 +++++ .../ExpectedParamAuthenticatorFactory.java | 128 +++++++ .../PushButtonAuthenticator.java | 90 +++++ .../PushButtonAuthenticatorFactory.java | 109 ++++++ ...ycloak.authentication.AuthenticatorFactory | 4 +- .../admin/authentication/ProvidersTest.java | 4 + .../forms/AuthenticatorSubflowsTest.java | 350 ++++++++++++++++++ .../base/src/test/resources/log4j.properties | 2 + .../src/test/resources/log4j.properties | 3 +- 10 files changed, 790 insertions(+), 4 deletions(-) create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticator.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticatorFactory.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticator.java create mode 100644 testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticatorFactory.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AuthenticatorSubflowsTest.java diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index 3a9c53cf2a..89471e7a8f 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -121,7 +121,20 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { if (model.isAuthenticatorFlow()) { logger.debug("execution is flow"); AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); - Response flowChallenge = authenticationFlow.processFlow(); + + Response flowChallenge = null; + try { + flowChallenge = authenticationFlow.processFlow(); + } catch (AuthenticationFlowException afe) { + if (model.isAlternative()) { + logger.debug("Thrown exception in alternative Subflow. Ignoring Subflow"); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED); + continue; + } else { + throw afe; + } + } + if (flowChallenge == null) { processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); if (model.isAlternative()) alternativeSuccessful = true; @@ -183,7 +196,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { // if (redirect != null) return redirect; AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions); - logger.debug("invoke authenticator.authenticate"); + logger.debugv("invoke authenticator.authenticate: {0}", factory.getId()); authenticator.authenticate(context); Response response = processResult(context, false); if (response != null) return response; diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticator.java new file mode 100644 index 0000000000..08b475fbbb --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticator.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017 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.testsuite.authentication; + +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; +import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; + +/** + * @author Marek Posolda + */ +public class ExpectedParamAuthenticator implements Authenticator { + + public static final String EXPECTED_VALUE = "expected_value"; + + public static final String LOGGED_USER = "logged_user"; + + + private static final Logger logger = Logger.getLogger(ExpectedParamAuthenticator.class); + + @Override + public void authenticate(AuthenticationFlowContext context) { + String paramValue = context.getAuthenticationSession().getClientNote(AuthorizationEndpoint.LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + "foo"); + String expectedValue = context.getAuthenticatorConfig().getConfig().get(EXPECTED_VALUE); + logger.info("Value: " + paramValue + ", expectedValue: " + expectedValue); + + if (paramValue != null && paramValue.equals(expectedValue)) { + + String loggedUser = context.getAuthenticatorConfig().getConfig().get(LOGGED_USER); + if (loggedUser == null) { + logger.info("Successfully authenticated, but don't set any authenticated user"); + } else { + UserModel user = context.getSession().users().getUserByUsername(loggedUser, context.getRealm()); + logger.info("Successfully authenticated as user " + user.getUsername()); + context.setUser(user); + } + + context.success(); + } else { + context.attempted(); + } + } + + @Override + public void action(AuthenticationFlowContext context) { + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + + } + + + @Override + public void close() { + + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticatorFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticatorFactory.java new file mode 100644 index 0000000000..2a10ab1caa --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/ExpectedParamAuthenticatorFactory.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 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.testsuite.authentication; + +import java.util.ArrayList; +import java.util.List; + +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; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ExpectedParamAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { + + public static final String PROVIDER_ID = "expected-param-authenticator"; + + private static final ExpectedParamAuthenticator SINGLETON = new ExpectedParamAuthenticator(); + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public Authenticator create(KeycloakSession session) { + return SINGLETON; + } + + private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public String getHelpText() { + return "You will be approved if you send query string parameter 'foo' with expected value."; + } + + @Override + public String getDisplayType() { + return "TEST: Expected Parameter"; + } + + @Override + public String getReferenceCategory() { + return "Expected Parameter"; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName(ExpectedParamAuthenticator.EXPECTED_VALUE); + property.setLabel("Expected query parameter value"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("Expected value of query parameter foo. Authenticator will success if request to OIDC authz endpoint has this parameter"); + configProperties.add(property); + + property = new ProviderConfigProperty(); + property.setName(ExpectedParamAuthenticator.LOGGED_USER); + property.setLabel("Automatically logged user"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("This user will be successfully authenticated automatically when present"); + configProperties.add(property); + } + + + @Override + public List getConfigProperties() { + return configProperties; + } + + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticator.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticator.java new file mode 100644 index 0000000000..bb7dcd3266 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticator.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 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.testsuite.authentication; + +import javax.ws.rs.core.Response; + +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; + +/** + * @author Marek Posolda + */ +public class PushButtonAuthenticator implements Authenticator { + + @Override + public void authenticate(AuthenticationFlowContext context) { + String accessCode = context.generateAccessCode(); + String actionUrl = context.getActionUrl(accessCode).toString(); + + StringBuilder response = new StringBuilder("PushTheButton"); + + UserModel user = context.getUser(); + if (user == null) { + response.append("No authenticated user
"); + } else { + response.append("Authenticated user: " + user.getUsername() + "
"); + } + + response.append("
"); + response.append(" This is the Test Approver. Press login to continue.
"); + response.append(" "); + response.append("
"); + String html = response.toString(); + + Response jaxrsResponse = Response + .status(Response.Status.OK) + .type("text/html") + .entity(html) + .build(); + + context.challenge(jaxrsResponse); + +// Response challenge = context.form().createForm("login-approve.ftl"); +// context.challenge(challenge); + } + + @Override + public void action(AuthenticationFlowContext context) { + context.success(); + } + + @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() { + + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticatorFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticatorFactory.java new file mode 100644 index 0000000000..84b177be14 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authentication/PushButtonAuthenticatorFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright 2017 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.testsuite.authentication; + +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 Bill Burke + * @version $Revision: 1 $ + */ +public class PushButtonAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { + + public static final String PROVIDER_ID = "push-button-authenticator"; + private static final PushButtonAuthenticator SINGLETON = new PushButtonAuthenticator(); + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public Authenticator create(KeycloakSession session) { + return SINGLETON; + } + + private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public String getHelpText() { + return "Just press the button to login."; + } + + @Override + public String getDisplayType() { + return "TEST: Button Login"; + } + + @Override + public String getReferenceCategory() { + return "Button Login"; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + private static final List configProperties = new ArrayList(); + + @Override + public List getConfigProperties() { + return configProperties; + } + + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 660f91ff4d..18317d048c 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -17,4 +17,6 @@ org.keycloak.testsuite.forms.PassThroughAuthenticator org.keycloak.testsuite.forms.PassThroughRegistration -org.keycloak.testsuite.forms.ClickThroughAuthenticator \ No newline at end of file +org.keycloak.testsuite.forms.ClickThroughAuthenticator +org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory +org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java index 5a50b081ba..e13794dc8e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java @@ -151,6 +151,8 @@ public class ProvidersTest extends AbstractAuthenticationTest { "Validates the password supplied as a 'password' form parameter in direct grant request"); addProviderInfo(result, "direct-grant-validate-username", "Username Validation", "Validates the username supplied as a 'username' form parameter in direct grant request"); + addProviderInfo(result, "expected-param-authenticator", "TEST: Expected Parameter", + "You will be approved if you send query string parameter 'foo' with expected value."); addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header"); addProviderInfo(result, "identity-provider-redirector", "Identity Provider Redirector", "Redirects to default Identity Provider or Identity Provider specified with kc_idp_hint query parameter"); addProviderInfo(result, "idp-confirm-link", "Confirm link existing account", "Show the form where user confirms if he wants " + @@ -163,6 +165,8 @@ public class ProvidersTest extends AbstractAuthenticationTest { "User reviews and updates profile data retrieved from Identity Provider in the displayed form"); addProviderInfo(result, "idp-username-password-form", "Username Password Form for identity provider reauthentication", "Validates a password from login form. Username is already known from identity provider authentication"); + addProviderInfo(result, "push-button-authenticator", "TEST: Button Login", + "Just press the button to login."); addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response."); addProviderInfo(result, "reset-credentials-choose-user", "Choose User", "Choose a user to reset credentials for"); addProviderInfo(result, "reset-otp", "Reset OTP", "Sets the Configure OTP required action if execution is REQUIRED. " + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AuthenticatorSubflowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AuthenticatorSubflowsTest.java new file mode 100644 index 0000000000..d057ace07c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/AuthenticatorSubflowsTest.java @@ -0,0 +1,350 @@ +/* + * Copyright 2017 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.testsuite.forms; + +import java.util.HashMap; +import java.util.Map; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; +import org.keycloak.events.Details; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.authentication.ExpectedParamAuthenticator; +import org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory; +import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.runonserver.RunOnServerDeployment; +import org.openqa.selenium.By; + +/** + * @author Marek Posolda + */ +public class AuthenticatorSubflowsTest extends AbstractTestRealmKeycloakTest { + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected ErrorPage errorPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Deployment + public static WebArchive deploy() { + return RunOnServerDeployment.create(UserResource.class) + .addPackages(true, "org.keycloak.testsuite"); + } + + + @Before + public void setupFlows() { + testingClient.server().run(session -> { + RealmModel realm = session.realms().getRealmByName("test"); + + if (realm.getBrowserFlow().getAlias().equals("parent-flow")) { + return; + } + + // Parent flow + AuthenticationFlowModel browser = new AuthenticationFlowModel(); + browser.setAlias("parent-flow"); + browser.setDescription("browser based authentication"); + browser.setProviderId("basic-flow"); + browser.setTopLevel(true); + browser.setBuiltIn(true); + browser = realm.addAuthenticationFlow(browser); + realm.setBrowserFlow(browser); + + // Subflow1 + AuthenticationFlowModel subflow1 = new AuthenticationFlowModel(); + subflow1.setTopLevel(false); + subflow1.setBuiltIn(true); + subflow1.setAlias("subflow-1"); + subflow1.setDescription("Parameter 'foo=bar1' AND username+password"); + subflow1.setProviderId("basic-flow"); + subflow1 = realm.addAuthenticationFlow(subflow1); + + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setFlowId(subflow1.getId()); + execution.setPriority(10); + execution.setAuthenticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // Subflow1 - foo=bar1 + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow1.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(ExpectedParamAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + AuthenticatorConfigModel configModel = new AuthenticatorConfigModel(); + configModel.setAlias("bar1"); + Map config = new HashMap<>(); + config.put(ExpectedParamAuthenticator.EXPECTED_VALUE, "bar1"); + configModel.setConfig(config); + configModel = realm.addAuthenticatorConfig(configModel); + execution.setAuthenticatorConfig(configModel.getId()); + + realm.addAuthenticatorExecution(execution); + + // Subflow1 - username password + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow1.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(UsernamePasswordFormFactory.PROVIDER_ID); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + + + // Subflow2 + AuthenticationFlowModel subflow2 = new AuthenticationFlowModel(); + subflow2.setTopLevel(false); + subflow2.setBuiltIn(true); + subflow2.setAlias("subflow-2"); + subflow2.setDescription("username+password AND pushButton"); + subflow2.setProviderId("basic-flow"); + subflow2 = realm.addAuthenticationFlow(subflow2); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setFlowId(subflow2.getId()); + execution.setPriority(20); + execution.setAuthenticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // Subflow2 - push the button + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow2.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + // Subflow2 - username-password + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow2.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(UsernamePasswordFormFactory.PROVIDER_ID); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + // Subflow3 + AuthenticationFlowModel subflow3 = new AuthenticationFlowModel(); + subflow3.setTopLevel(false); + subflow3.setBuiltIn(true); + subflow3.setAlias("subflow-3"); + subflow3.setDescription("alternative subflow with child subflows"); + subflow3.setProviderId("basic-flow"); + subflow3 = realm.addAuthenticationFlow(subflow3); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setFlowId(subflow3.getId()); + execution.setPriority(30); + execution.setAuthenticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // Subflow3-1 + AuthenticationFlowModel subflow31 = new AuthenticationFlowModel(); + subflow31.setTopLevel(false); + subflow31.setBuiltIn(true); + subflow31.setAlias("subflow-31"); + subflow31.setDescription("subflow-31"); + subflow31.setProviderId("basic-flow"); + subflow31 = realm.addAuthenticationFlow(subflow31); + + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow3.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setFlowId(subflow31.getId()); + execution.setPriority(10); + execution.setAuthenticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // Subflow3-1 - foo=bar2 + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow31.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(ExpectedParamAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(10); + execution.setAuthenticatorFlow(false); + + configModel = new AuthenticatorConfigModel(); + configModel.setAlias("bar2"); + config = new HashMap<>(); + config.put(ExpectedParamAuthenticator.EXPECTED_VALUE, "bar2"); + config.put(ExpectedParamAuthenticator.LOGGED_USER, "john-doh@localhost"); + configModel.setConfig(config); + configModel = realm.addAuthenticatorConfig(configModel); + execution.setAuthenticatorConfig(configModel.getId()); + + realm.addAuthenticatorExecution(execution); + + // Subflow3-1 - push the button + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow31.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(PushButtonAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + + realm.addAuthenticatorExecution(execution); + + // Subflow3 - foo=bar3 + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(subflow3.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setAuthenticator(ExpectedParamAuthenticatorFactory.PROVIDER_ID); + execution.setPriority(20); + execution.setAuthenticatorFlow(false); + + configModel = new AuthenticatorConfigModel(); + configModel.setAlias("bar3"); + config = new HashMap<>(); + config.put(ExpectedParamAuthenticator.EXPECTED_VALUE, "bar3"); + config.put(ExpectedParamAuthenticator.LOGGED_USER, "keycloak-user@localhost"); + configModel.setConfig(config); + configModel = realm.addAuthenticatorConfig(configModel); + execution.setAuthenticatorConfig(configModel.getId()); + + realm.addAuthenticatorExecution(execution); + + + }); + } + + +// @Test +// public void testSleep() throws Exception { +// log.info("Start sleeping"); +// Thread.sleep(1000000); +// } + + + @Test + public void testSubflow1() throws Exception { + // Add foo=bar1 . I am redirected to subflow1 - username+password form + String loginFormUrl = oauth.getLoginFormUrl(); + loginFormUrl = loginFormUrl + "&foo=bar1"; + log.info("loginFormUrl: " + loginFormUrl); + + //Thread.sleep(10000000); + + driver.navigate().to(loginFormUrl); + + loginPage.assertCurrent(); + + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + + @Test + public void testSubflow2() throws Exception { + // Don't add 'foo' parameter. I am redirected to subflow2 - push the button + String loginFormUrl = oauth.getLoginFormUrl(); + log.info("loginFormUrl: " + loginFormUrl); + + //Thread.sleep(10000000); + + driver.navigate().to(loginFormUrl); + + Assert.assertEquals("PushTheButton", driver.getTitle()); + + // Push the button. I am redirected to username+password form + driver.findElement(By.name("submit1")).click(); + + + loginPage.assertCurrent(); + + // Fill username+password. I am successfully authenticated + oauth.fillLoginForm("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + +// @Test +// public void testSubflow31() { +// // Fill foo=bar2. I am see the pushButton +// String loginFormUrl = oauth.getLoginFormUrl(); +// loginFormUrl = loginFormUrl + "&foo=bar2"; +// log.info("loginFormUrl: " + loginFormUrl); +// +// //Thread.sleep(10000000); +// +// driver.navigate().to(loginFormUrl); +// Assert.assertEquals("PushTheButton", driver.getTitle()); +// +// // Confirm push button. I am authenticated as john-doh@localhost +// driver.findElement(By.name("submit1")).click(); +// +// appPage.assertCurrent(); +// +// events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").assertEvent(); +// } +// +// +// @Test +// public void testSubflow32() { +// // Fill foo=bar3. I am login automatically as "keycloak-user@localhost" +// +// +// } + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties index 9c9bfc259c..1c425196dc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties @@ -69,3 +69,5 @@ log4j.logger.org.apache.directory.server.core=warn # log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace # log4j.logger.org.keycloak.keys.infinispan=trace log4j.logger.org.keycloak.services.clientregistration.policy=debug + +#log4j.logger.org.keycloak.authentication=debug diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index 6439950f9f..5f0d60b153 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -91,4 +91,5 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error #log4j.logger.org.keycloak.protocol=debug #log4j.logger.org.keycloak.services.resources.LoginActionsService=debug #log4j.logger.org.keycloak.services.managers=debug -#log4j.logger.org.keycloak.services.resources.SessionCodeChecks=debug \ No newline at end of file +#log4j.logger.org.keycloak.services.resources.SessionCodeChecks=debug +#log4j.logger.org.keycloak.authentication=debug \ No newline at end of file From 955cbc76d78b92c81255decd68cab06b4517d3a4 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Mon, 26 Jun 2017 09:55:21 +0200 Subject: [PATCH 3/3] KEYCLOAK-5030 Change action tokens cache type to distributed --- .../content/bin/migrate-domain-clustered.cli | 14 +++---- .../content/bin/migrate-standalone-ha.cli | 14 +++---- .../src/main/cli/keycloak-install-ha-base.cli | 6 +-- ...ltInfinispanConnectionProviderFactory.java | 37 +++++++++++++++++-- .../InfinispanActionTokenStoreProvider.java | 8 ++-- ...nispanActionTokenStoreProviderFactory.java | 23 +++++------- .../jboss/common/add-keycloak-caches.xsl | 2 + .../crossdc/AbstractAdminCrossDCTest.java | 1 - .../crossdc/AbstractCrossDCTest.java | 8 ++-- .../crossdc/ActionTokenCrossDCTest.java | 18 ++++----- .../base/src/test/resources/arquillian.xml | 2 +- .../keycloak-infinispan.xml | 4 +- .../keycloak-infinispan2.xml | 4 +- 13 files changed, 84 insertions(+), 57 deletions(-) diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli index 0f477458de..934421794e 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli @@ -206,13 +206,13 @@ if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache- echo end-if -if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource - echo Adding local-cache=actionTokens to keycloak cache container... - /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) - /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) - /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) - /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) - /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) +if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/:read-resource + echo Adding distributed-cache=actionTokens to keycloak cache container... + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/:add(indexing=NONE,mode=SYNC,owners=2) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) echo end-if diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli index 4d5fac67db..4f4e3e09fa 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli @@ -211,13 +211,13 @@ if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distrib echo end-if -if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource - echo Adding local-cache=actionTokens to keycloak cache container... - /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) - /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) - /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) - /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) - /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/:read-resource + echo Adding distributed-cache=actionTokens to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/:add(indexing=NONE,mode=SYNC,owners=2) + /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) echo end-if diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli index 4710eb8a82..6bb11d57d4 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli @@ -16,7 +16,7 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=keycloak/local-cache=keys:add() /subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000) -/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens:add() -/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE) -/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000) +/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:add(indexing="NONE",mode="SYNC",owners="2") +/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE) +/subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 53e496f06c..a9df0471c1 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -248,7 +248,14 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig()); cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true); - cacheManager.defineConfiguration(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, getActionTokenCacheConfig()); + final ConfigurationBuilder actionTokenCacheConfigBuilder = getActionTokenCacheConfig(); + if (clustered) { + actionTokenCacheConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); + } + if (jdgEnabled) { + configureRemoteActionTokenCacheStore(actionTokenCacheConfigBuilder, async); + } + cacheManager.defineConfiguration(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, actionTokenCacheConfigBuilder.build()); cacheManager.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, true); long authzRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME).getCacheConfiguration().eviction().maxEntries(); @@ -301,6 +308,30 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon } + private void configureRemoteActionTokenCacheStore(ConfigurationBuilder builder, boolean async) { + String jdgServer = config.get("remoteStoreServer", "localhost"); + Integer jdgPort = config.getInt("remoteStorePort", 11222); + + builder.persistence() + .passivation(false) + .addStore(RemoteStoreConfigurationBuilder.class) + .fetchPersistentState(false) + .ignoreModifications(false) + .purgeOnStartup(false) + .preload(true) + .shared(true) + .remoteCacheName(InfinispanConnectionProvider.ACTION_TOKEN_CACHE) + .rawValues(true) + .forceReturnValues(false) + .marshaller(KeycloakHotRodMarshallerFactory.class.getName()) + .addServer() + .host(jdgServer) + .port(jdgPort) + .async() + .enabled(async); + + } + protected Configuration getKeysCacheConfig() { ConfigurationBuilder cb = new ConfigurationBuilder(); cb.eviction().strategy(EvictionStrategy.LRU).type(EvictionType.COUNT).size(InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX); @@ -308,7 +339,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return cb.build(); } - private Configuration getActionTokenCacheConfig() { + private ConfigurationBuilder getActionTokenCacheConfig() { ConfigurationBuilder cb = new ConfigurationBuilder(); cb.eviction() @@ -319,7 +350,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon .maxIdle(InfinispanConnectionProvider.ACTION_TOKEN_MAX_IDLE_SECONDS, TimeUnit.SECONDS) .wakeUpInterval(InfinispanConnectionProvider.ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS, TimeUnit.SECONDS); - return cb.build(); + return cb; } private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java index f02fb5a403..b4689aa743 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java @@ -17,13 +17,14 @@ package org.keycloak.models.sessions.infinispan; import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.Time; import org.keycloak.models.*; -import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent; import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; import java.util.*; +import java.util.concurrent.TimeUnit; import org.infinispan.Cache; /** @@ -57,9 +58,7 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi ActionTokenReducedKey tokenKey = new ActionTokenReducedKey(key.getUserId(), key.getActionId(), key.getActionVerificationNonce()); ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes); - ClusterProvider cluster = session.getProvider(ClusterProvider.class); - AddInvalidatedActionTokenEvent event = new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue); - this.tx.notify(cluster, generateActionTokenEventId(), event, false); + this.tx.put(actionKeyCache, tokenKey, tokenValue, key.getExpiration() - Time.currentTime(), TimeUnit.SECONDS); } private static String generateActionTokenEventId() { @@ -92,6 +91,7 @@ public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvi return value; } + @Override public void removeAll(String userId, String actionId) { if (userId == null || actionId == null) { return; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java index f67f28f6c0..95ee903507 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java @@ -19,16 +19,16 @@ package org.keycloak.models.sessions.infinispan; import org.keycloak.Config; import org.keycloak.Config.Scope; import org.keycloak.cluster.ClusterProvider; -import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.*; -import org.keycloak.models.cache.infinispan.events.AddInvalidatedActionTokenEvent; import org.keycloak.models.cache.infinispan.events.RemoveActionTokensSpecificEvent; import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; +import java.util.List; import java.util.Objects; -import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.context.Flag; import org.infinispan.remoting.transport.Address; @@ -76,22 +76,17 @@ public class InfinispanActionTokenStoreProviderFactory implements ActionTokenSto LOG.debugf("[%s] Removing token invalidation for user+action: userId=%s, actionId=%s", cacheAddress, e.getUserId(), e.getActionId()); - cache + AdvancedCache localCache = cache .getAdvancedCache() - .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD) + .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD); + + List toRemove = localCache .keySet() .stream() .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) - .forEach(cache::remove); - } else if (event instanceof AddInvalidatedActionTokenEvent) { - AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event; + .collect(Collectors.toList()); - LOG.debugf("[%s] Invalidating token %s", cacheAddress, e.getKey()); - if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) { - cache.put(e.getKey(), e.getTokenValue()); - } else { - cache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS); - } + toRemove.forEach(localCache::remove); } }); diff --git a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl index ee9c29d419..540b4b5f57 100644 --- a/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl +++ b/testsuite/integration-arquillian/servers/cache-server/jboss/common/add-keycloak-caches.xsl @@ -30,6 +30,7 @@ + @@ -38,6 +39,7 @@ + diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java index 2baa336cf9..2ad3cc3517 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java @@ -28,7 +28,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import org.hamcrest.Matcher; import org.junit.Before; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java index c88c0c14aa..b4d4236a6d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractCrossDCTest.java @@ -116,11 +116,11 @@ public abstract class AbstractCrossDCTest extends AbstractTestRealmKeycloakTest * @return */ protected Keycloak getAdminClientFor(ContainerInfo node) { - Keycloak adminClient = backendAdminClients.get(node); - if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) { - adminClient = this.adminClient; + Keycloak client = backendAdminClients.get(node); + if (client == null && node.equals(suiteContext.getAuthServerInfo())) { + client = this.adminClient; } - return adminClient; + return client; } /** diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java index dbef2fcc9b..972be313dc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/ActionTokenCrossDCTest.java @@ -34,7 +34,6 @@ import javax.mail.internet.MimeMessage; import javax.ws.rs.core.Response; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -45,7 +44,6 @@ import org.keycloak.testsuite.arquillian.InfinispanStatistics.Constants; import java.util.concurrent.TimeUnit; import org.hamcrest.Matchers; import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; /** @@ -117,21 +115,23 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest { old -> greaterThan((Comparable) 0l) ); - // Verify that the caches are synchronized - assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries)); - assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), - is(cacheDc1Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES))); - assertEquals("Your account has been updated.", driver.getTitle()); + // Verify that there was an action token added in the node which was targetted by the link + assertThat(cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES), greaterThan(originalNumberOfEntries)); + disableDcOnLoadBalancer(0); enableDcOnLoadBalancer(1); - driver.navigate().to(link); + // Make sure that after going to the link, the invalidated action token has been retrieved from Infinispan server cluster in the other DC + assertSingleStatistics(cacheDc1Node0Statistics, Constants.STAT_CACHE_NUMBER_OF_ENTRIES, + () -> driver.navigate().to(link), + Matchers::greaterThan + ); + errorPage.assertCurrent(); } - @Ignore("KEYCLOAK-5030") @Test public void sendResetPasswordEmailAfterNewNodeAdded() throws IOException, MessagingException { disableDcOnLoadBalancer(1); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index f9919d5867..58ef272b7c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -224,7 +224,7 @@ org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer localhost ${auth.server.http.port} - auth-server-undertow-cross-dc-0.1=http://localhost:8101,auth-server-undertow-cross-dc-0.2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1.1=http://localhost:8111,auth-server-undertow-cross-dc-1.2-manual=http://localhost:8112 + auth-server-undertow-cross-dc-0_1=http://localhost:8101,auth-server-undertow-cross-dc-0_2-manual=http://localhost:8102,auth-server-undertow-cross-dc-1_1=http://localhost:8111,auth-server-undertow-cross-dc-1_2-manual=http://localhost:8112 diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index 818626a8fb..476449ab9c 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -113,10 +113,10 @@ - + - + diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml index 839ecdf2df..ca801028f3 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml @@ -116,10 +116,10 @@ - + - +