diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java index eabe412..c95b7cb 100644 --- a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/Assert.java @@ -21,6 +21,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -33,6 +34,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; @@ -64,6 +66,27 @@ private static String[] names(List list) { return names; } + public static void assertProviderConfigProperty(ConfigPropertyRepresentation property, String name, String label, String defaultValue, String helpText, String type) { + Assert.assertEquals(name, property.getName()); + Assert.assertEquals(label, property.getLabel()); + Assert.assertEquals(defaultValue, property.getDefaultValue()); + Assert.assertEquals(helpText, property.getHelpText()); + Assert.assertEquals(type, property.getType()); + } + + public static void assertMap(Map config, String... expected) { + if (expected == null) { + expected = new String[] {}; + } + + Assert.assertEquals(config.size() * 2, expected.length); + for (int i=0 ; iMarko Strukelj + */ +public abstract class AbstractAuthenticationTest extends AbstractAdminClientTest { + + static final String REALM_NAME = "test"; + + static final String REQUIRED = "REQUIRED"; + static final String CONDITIONAL = "CONDITIONAL"; + static final String DISABLED = "DISABLED"; + static final String ALTERNATIVE = "ALTERNATIVE"; + + RealmResource realmResource; + AuthenticationManagementResource authMgmtResource; + protected String testRealmId; + + + @BeforeEach + public void before() { + realmResource = adminClient.realms().realm(REALM_NAME); + authMgmtResource = realmResource.flows(); + testRealmId = realmResource.toRepresentation().getId(); + } + + + public static AuthenticationExecutionInfoRepresentation findExecutionByProvider(String provider, List executions) { + for (AuthenticationExecutionInfoRepresentation exec : executions) { + if (provider.equals(exec.getProviderId())) { + return exec; + } + } + return null; + } + + /** + * Searches for an execution located before the provided execution on the same level of + * an authentication flow. + * + * @param execution execution to find a neighbor for + * @param executions list of executions to search in + * @return execution, or null if not found + */ + public static AuthenticationExecutionInfoRepresentation findPreviousExecution(AuthenticationExecutionInfoRepresentation execution, List executions) { + for (AuthenticationExecutionInfoRepresentation exec : executions) { + if (exec.getLevel() != execution.getLevel()) { + continue; + } + if (exec.getIndex() == execution.getIndex() - 1) { + return exec; + } + } + return null; + } + + public static AuthenticationFlowRepresentation findFlowByAlias(String alias, List flows) { + for (AuthenticationFlowRepresentation flow : flows) { + if (alias.equals(flow.getAlias())) { + return flow; + } + } + return null; + } + + void compareExecution(AuthenticationExecutionInfoRepresentation expected, AuthenticationExecutionInfoRepresentation actual) { + assertEquals(expected.getRequirement(), actual.getRequirement(), "Execution requirement - " + actual.getProviderId()); + assertEquals(expected.getDisplayName(), actual.getDisplayName(), "Execution display name - " + actual.getProviderId()); + assertEquals(expected.getConfigurable(), actual.getConfigurable(), "Execution configurable - " + actual.getProviderId()); + assertEquals(expected.getProviderId(), actual.getProviderId(), "Execution provider id - " + actual.getProviderId()); + assertEquals(expected.getLevel(), actual.getLevel(), "Execution level - " + actual.getProviderId()); + assertEquals(expected.getAuthenticationFlow(), actual.getAuthenticationFlow(), "Execution authentication flow - " + actual.getProviderId()); + assertEquals(expected.getRequirementChoices(), actual.getRequirementChoices(), "Execution requirement choices - " + actual.getProviderId()); + } + + void compareExecution(AuthenticationExecutionExportRepresentation expected, AuthenticationExecutionExportRepresentation actual) { + assertEquals(expected.getFlowAlias(), actual.getFlowAlias(), "Execution flowAlias - " + actual.getFlowAlias()); + assertEquals(expected.getAuthenticator(), actual.getAuthenticator(), "Execution authenticator - " + actual.getAuthenticator()); + assertEquals(expected.isUserSetupAllowed(), actual.isUserSetupAllowed(), "Execution userSetupAllowed - " + actual.getAuthenticator()); + assertEquals(expected.isAuthenticatorFlow(), actual.isAuthenticatorFlow(), "Execution authenticatorFlow - " + actual.getAuthenticator()); + assertEquals(expected.getAuthenticatorConfig(), actual.getAuthenticatorConfig(), "Execution authenticatorConfig - " + actual.getAuthenticatorConfig()); + assertEquals(expected.getPriority(), actual.getPriority(), "Execution priority - " + actual.getAuthenticator()); + assertEquals(expected.getRequirement(), actual.getRequirement(), "Execution requirement - " + actual.getAuthenticator()); + } + + void compareExecutions(List expected, List actual) { + assertNotNull(actual, "Executions should not be null"); + assertEquals(expected.size(), actual.size(), "Size"); + + for (int i = 0; i < expected.size(); i++) { + compareExecution(expected.get(i), actual.get(i)); + } + } + + void compareFlows(AuthenticationFlowRepresentation expected, AuthenticationFlowRepresentation actual) { + assertEquals(expected.getAlias(), actual.getAlias(), "Flow alias"); + assertEquals(expected.getDescription(), actual.getDescription(), "Flow description"); + assertEquals(expected.getProviderId(), actual.getProviderId(), "Flow providerId"); + assertEquals(expected.isTopLevel(), actual.isTopLevel(), "Flow top level"); + assertEquals(expected.isBuiltIn(), actual.isBuiltIn(), "Flow built-in"); + + List expectedExecs = expected.getAuthenticationExecutions(); + List actualExecs = actual.getAuthenticationExecutions(); + + if (expectedExecs == null) { + assertTrue(actualExecs == null || actualExecs.size() == 0, "Executions should be null or empty"); + } else { + compareExecutions(expectedExecs, actualExecs); + } + } + + AuthenticationFlowRepresentation newFlow(String alias, String description, + String providerId, boolean topLevel, boolean builtIn) { + AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation(); + flow.setAlias(alias); + flow.setDescription(description); + flow.setProviderId(providerId); + flow.setTopLevel(topLevel); + flow.setBuiltIn(builtIn); + return flow; + } + + AuthenticationExecutionInfoRepresentation newExecInfo(String displayName, String providerId, Boolean configurable, + int level, int index, String requirement, Boolean authFlow, String[] choices, + int priority) { + + AuthenticationExecutionInfoRepresentation execution = new AuthenticationExecutionInfoRepresentation(); + execution.setRequirement(requirement); + execution.setDisplayName(displayName); + execution.setConfigurable(configurable); + execution.setProviderId(providerId); + execution.setLevel(level); + execution.setIndex(index); + execution.setAuthenticationFlow(authFlow); + execution.setPriority(priority); + if (choices != null) { + execution.setRequirementChoices(Arrays.asList(choices)); + } + return execution; + } + + void addExecInfo(List target, String displayName, String providerId, Boolean configurable, + int level, int index, String requirement, Boolean authFlow, String[] choices, int priority) { + + AuthenticationExecutionInfoRepresentation exec = newExecInfo(displayName, providerId, configurable, level, index, requirement, authFlow, choices, priority); + target.add(exec); + } + + AuthenticatorConfigRepresentation newConfig(String alias, String[] keyvalues) { + AuthenticatorConfigRepresentation config = new AuthenticatorConfigRepresentation(); + config.setAlias(alias); + + if (keyvalues == null) { + throw new IllegalArgumentException("keyvalues == null"); + } + if (keyvalues.length % 2 != 0) { + throw new IllegalArgumentException("keyvalues should have even number of elements"); + } + + LinkedHashMap params = new LinkedHashMap<>(); + for (int i = 0; i < keyvalues.length; i += 2) { + params.put(keyvalues[i], keyvalues[i + 1]); + } + config.setConfig(params); + return config; + } + + String createFlow(AuthenticationFlowRepresentation flowRep) { + Response response = authMgmtResource.createFlow(flowRep); + assertEquals(201, response.getStatus()); + response.close(); + String flowId = ApiUtil.getCreatedId(response); + getCleanup("test").addAuthenticationFlowId(flowId); + return flowId; + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/AuthenticatorConfigTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/AuthenticatorConfigTest.java new file mode 100644 index 0000000..b3e7f44 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/AuthenticatorConfigTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.keycloak.client.testsuite.Assert; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; + +import org.keycloak.testsuite.util.ApiUtil; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Marek Posolda + */ +public class AuthenticatorConfigTest extends AbstractAuthenticationTest { + + private String executionId; + + @BeforeEach + public void beforeConfigTest() { + AuthenticationFlowRepresentation flowRep = newFlow("firstBrokerLogin2", "firstBrokerLogin2", "basic-flow", true, false); + createFlow(flowRep); + + HashMap params = new HashMap<>(); + params.put("provider", "idp-create-user-if-unique"); + authMgmtResource.addExecution("firstBrokerLogin2", params); + + List executionReps = authMgmtResource.getExecutions("firstBrokerLogin2"); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("idp-create-user-if-unique", executionReps); + assertNotNull(exec); + executionId = exec.getId(); + } + + @Test + public void testCreateConfigWithReservedChar() { + AuthenticatorConfigRepresentation cfg = newConfig("f!oo", "require.password.update.after.registration", "true"); + Response resp = authMgmtResource.newExecutionConfig(executionId, cfg); + assertEquals(400, resp.getStatus()); + } + + @Test + public void testCreateConfig() { + AuthenticatorConfigRepresentation cfg = newConfig("foo", "require.password.update.after.registration", "true"); + + // Attempt to create config for non-existent execution + Response response = authMgmtResource.newExecutionConfig("exec-id-doesnt-exists", cfg); + assertEquals(404, response.getStatus()); + response.close(); + + // Create config success + String cfgId = createConfig(executionId, cfg); + + // Assert found + AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); + assertConfig(cfgRep, cfgId, "foo", "require.password.update.after.registration", "true"); + + // Cleanup + authMgmtResource.removeAuthenticatorConfig(cfgId); + } + + @Test + public void testUpdateConfigWithBadChar() { + try { + AuthenticatorConfigRepresentation cfg = newConfig("foo", "require.password.update.after.registration", "true"); + String cfgId = createConfig(executionId, cfg); + AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); + + cfgRep.setAlias("Bad@Char"); + authMgmtResource.updateAuthenticatorConfig(cfgRep.getId(), cfgRep); + fail(); + } + catch (BadRequestException e) { + } + } + + @Test + public void testUpdateConfig() { + AuthenticatorConfigRepresentation cfg = newConfig("foo", "require.password.update.after.registration", "true"); + String cfgId = createConfig(executionId, cfg); + AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); + + // Try to update not existent config + try { + authMgmtResource.updateAuthenticatorConfig("not-existent", cfgRep); + fail("Config didn't found"); + } catch (NotFoundException nfe) { + // Expected + } + + // Assert nothing changed + cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); + assertConfig(cfgRep, cfgId, "foo", "require.password.update.after.registration", "true"); + + // Update success + cfgRep.setAlias("foo2"); + cfgRep.getConfig().put("configKey2", "configValue2"); + authMgmtResource.updateAuthenticatorConfig(cfgRep.getId(), cfgRep); + + // Assert updated + cfgRep = authMgmtResource.getAuthenticatorConfig(cfgRep.getId()); + assertConfig(cfgRep, cfgId, "foo2", + "require.password.update.after.registration", "true", + "configKey2", "configValue2"); + } + + + @Test + public void testRemoveConfig() { + AuthenticatorConfigRepresentation cfg = newConfig("foo", "require.password.update.after.registration", "true"); + String cfgId = createConfig(executionId, cfg); + AuthenticatorConfigRepresentation cfgRep = authMgmtResource.getAuthenticatorConfig(cfgId); + + // Assert execution has our config + AuthenticationExecutionInfoRepresentation execution = findExecutionByProvider( + "idp-create-user-if-unique", authMgmtResource.getExecutions("firstBrokerLogin2")); + assertEquals(cfgRep.getId(), execution.getAuthenticationConfig()); + + + // Test remove not-existent + try { + authMgmtResource.removeAuthenticatorConfig("not-existent"); + fail("Config didn't found"); + } catch (NotFoundException nfe) { + // Expected + } + + // Test remove our config + authMgmtResource.removeAuthenticatorConfig(cfgId); + + // Assert config not found + try { + authMgmtResource.getAuthenticatorConfig(cfgRep.getId()); + fail("Not expected to find config"); + } catch (NotFoundException nfe) { + // Expected + } + + // Assert execution doesn't have our config + execution = findExecutionByProvider( + "idp-create-user-if-unique", authMgmtResource.getExecutions("firstBrokerLogin2")); + assertNull(execution.getAuthenticationConfig()); + } + + @Test + public void testNullsafetyIterationOverProperties() { + String providerId = "auth-cookie"; + String providerName = "Cookie"; + AuthenticatorConfigInfoRepresentation description = authMgmtResource.getAuthenticatorConfigDescription(providerId); + + assertEquals(providerName, description.getName()); + assertTrue(description.getProperties().isEmpty()); + } + + private String createConfig(String executionId, AuthenticatorConfigRepresentation cfg) { + Response resp = authMgmtResource.newExecutionConfig(executionId, cfg); + assertEquals(201, resp.getStatus()); + String cfgId = ApiUtil.getCreatedId(resp); + assertNotNull(cfgId); + return cfgId; + } + + private AuthenticatorConfigRepresentation newConfig(String alias, String cfgKey, String cfgValue) { + AuthenticatorConfigRepresentation cfg = new AuthenticatorConfigRepresentation(); + cfg.setAlias(alias); + Map cfgMap = new HashMap<>(); + cfgMap.put(cfgKey, cfgValue); + cfg.setConfig(cfgMap); + return cfg; + } + + private void assertConfig(AuthenticatorConfigRepresentation cfgRep, String id, String alias, String... fields) { + assertEquals(id, cfgRep.getId()); + assertEquals(alias, cfgRep.getAlias()); + Assert.assertMap(cfgRep.getConfig(), fields); + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ExecutionTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ExecutionTest.java new file mode 100644 index 0000000..50d2960 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ExecutionTest.java @@ -0,0 +1,352 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; + +import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; + +import org.keycloak.testsuite.util.ApiUtil; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.Matchers.hasItems; + +/** + * @author Marko Strukelj + */ +public class ExecutionTest extends AbstractAuthenticationTest { + + // KEYCLOAK-7975 + @Test + public void testUpdateAuthenticatorConfig() { + // copy built-in flow so we get a new editable flow + HashMap params = new HashMap<>(); + params.put("newName", "new-browser-flow"); + Response response = authMgmtResource.copy("browser", params); + try { + assertEquals(201, response.getStatus(), "Copy flow"); + } finally { + response.close(); + } + + // create Conditional OTP Form execution + params.put("provider", "auth-conditional-otp-form"); + authMgmtResource.addExecution("new-browser-flow", params); + + List executionReps = authMgmtResource.getExecutions("new-browser-flow"); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("auth-conditional-otp-form", executionReps); + + // create authenticator config for the execution + Map config = new HashMap<>(); + config.put("defaultOtpOutcome", "skip"); + config.put("otpControlAttribute", "test"); + config.put("forceOtpForHeaderPattern", ""); + config.put("forceOtpRole", ""); + config.put("noOtpRequiredForHeaderPattern", ""); + config.put("skipOtpRole", ""); + + AuthenticatorConfigRepresentation authConfigRep = new AuthenticatorConfigRepresentation(); + authConfigRep.setAlias("conditional-otp-form-config-alias"); + authConfigRep.setConfig(config); + response = authMgmtResource.newExecutionConfig(exec.getId(), authConfigRep); + + try { + authConfigRep.setId(ApiUtil.getCreatedId(response)); + } finally { + response.close(); + } + + // try to update the config adn check + config.put("otpControlAttribute", "test-updated"); + authConfigRep.setConfig(config); + authMgmtResource.updateAuthenticatorConfig(authConfigRep.getId(), authConfigRep); + + AuthenticatorConfigRepresentation updated = authMgmtResource.getAuthenticatorConfig(authConfigRep.getId()); + + assertThat(updated.getConfig().values(), hasItems("test-updated", "skip")); + } + + @Test + public void testAddRemoveExecution() { + + // try add execution to built-in flow + HashMap params = new HashMap<>(); + params.put("provider", "idp-review-profile"); + try { + authMgmtResource.addExecution("browser", params); + fail("add execution to built-in flow should fail"); + } catch (BadRequestException expected) { + // Expected + } + + // try add execution to not-existent flow + try { + authMgmtResource.addExecution("not-existent", params); + fail("add execution to not-existent flow should fail"); + } catch (BadRequestException expected) { + // Expected + } + + // copy built-in flow so we get a new editable flow + params.put("newName", "Copy-of-browser"); + Response response = authMgmtResource.copy("browser", params); + try { + assertEquals( 201, response.getStatus(), "Copy flow"); + } finally { + response.close(); + } + + // add execution using inexistent provider + params.put("provider", "test-execution"); + try { + authMgmtResource.addExecution("CopyOfBrowser", params); + fail("add execution with inexistent provider should fail"); + } catch(BadRequestException expected) { + // Expected + } + + // add execution - should succeed + params.put("provider", "idp-review-profile"); + authMgmtResource.addExecution("Copy-of-browser", params); + + // check execution was added + List executionReps = authMgmtResource.getExecutions("Copy-of-browser"); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("idp-review-profile", executionReps); + assertNotNull(exec); + + // we'll need auth-cookie later + AuthenticationExecutionInfoRepresentation authCookieExec = findExecutionByProvider("auth-cookie", executionReps); + + AuthenticationExecutionInfoRepresentation previousExecution = findPreviousExecution(exec, executionReps); + assertNotNull(previousExecution); + compareExecution(newExecInfo("Review Profile", "idp-review-profile", true, 0, 5, DISABLED, null, new String[]{REQUIRED, ALTERNATIVE,DISABLED}, previousExecution.getPriority() + 1), exec); + + // remove execution + authMgmtResource.removeExecution(exec.getId()); + + // check execution was removed + executionReps = authMgmtResource.getExecutions("Copy-of-browser"); + exec = findExecutionByProvider("idp-review-profile", executionReps); + assertNull(exec); + + // now add the execution again using a different method and representation + + // delete auth-cookie + authMgmtResource.removeExecution(authCookieExec.getId()); + + AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); + rep.setPriority(10); + rep.setAuthenticator("auth-cookie"); + rep.setRequirement(CONDITIONAL); + + // Should fail - missing parent flow + response = authMgmtResource.addExecution(rep); + try { + assertEquals( 400, response.getStatus(), "added execution missing parent flow"); + } finally { + response.close(); + } + + // Should fail - not existent parent flow + rep.setParentFlow("not-existent-id"); + response = authMgmtResource.addExecution(rep); + try { + assertEquals(400, response.getStatus(), "added execution missing parent flow"); + } finally { + response.close(); + } + + // Should fail - add execution to builtin flow + AuthenticationFlowRepresentation browserFlow = findFlowByAlias("browser", authMgmtResource.getFlows()); + rep.setParentFlow(browserFlow.getId()); + response = authMgmtResource.addExecution(rep); + try { + assertEquals(400, response.getStatus(), "added execution to builtin flow"); + } finally { + response.close(); + } + + // get Copy-of-browser flow id, and set it on execution + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation flow = findFlowByAlias("Copy-of-browser", flows); + rep.setParentFlow(flow.getId()); + + // add execution - should succeed + response = authMgmtResource.addExecution(rep); + try { + assertEquals(201, response.getStatus(), "added execution"); + } finally { + response.close(); + } + + // check execution was added + List executions = authMgmtResource.getExecutions("Copy-of-browser"); + exec = findExecutionByProvider("auth-cookie", executions); + assertNotNull(exec, "auth-cookie added"); + + // Note: there is no checking in addExecution if requirement is one of requirementChoices + // Thus we can have OPTIONAL which is neither ALTERNATIVE, nor DISABLED + compareExecution(newExecInfo("Cookie", "auth-cookie", false, 0, 0, CONDITIONAL, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED}, 10), exec); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void testUpdateExecution() { + + // get current auth-cookie execution + List executionReps = authMgmtResource.getExecutions("browser"); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider("auth-cookie", executionReps); + + assertEquals(ALTERNATIVE, exec.getRequirement(), "auth-cookie set to ALTERNATIVE"); + assertEquals(exec.getIndex(), 0, "auth-cookie is first in the flow"); + + // switch from DISABLED to ALTERNATIVE + exec.setRequirement(DISABLED); + exec.setPriority(Integer.MAX_VALUE); + authMgmtResource.updateExecutions("browser", exec); + + // make sure the change is visible + executionReps = authMgmtResource.getExecutions("browser"); + + // get current auth-cookie execution + AuthenticationExecutionInfoRepresentation exec2 = findExecutionByProvider("auth-cookie", executionReps); + + // The execution is expected to be last after priority change + long expectedIndex = executionReps.stream() + .filter(r -> r.getLevel() == exec2.getLevel()) + .count() - 1; + exec.setIndex(Math.toIntExact(expectedIndex)); + + compareExecution(exec, exec2); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void testClientFlowExecutions() { + // Create client flow + AuthenticationFlowRepresentation clientFlow = newFlow("new-client-flow", "desc", "client-flow", true, false); + createFlow(clientFlow); + + // Add execution to it + Map executionData = new HashMap<>(); + executionData.put("provider", "client-secret"); + authMgmtResource.addExecution("new-client-flow", executionData); + + // Check executions of not-existent flow - SHOULD FAIL + try { + authMgmtResource.getExecutions("not-existent"); + fail("Not expected to find executions"); + } catch (NotFoundException nfe) { + // Expected + } + + // Check existent executions + List executions = authMgmtResource.getExecutions("new-client-flow"); + AuthenticationExecutionInfoRepresentation executionRep = findExecutionByProvider("client-secret", executions); + assertNotNull(executionRep); + + // Update execution with not-existent flow - SHOULD FAIL + try { + authMgmtResource.updateExecutions("not-existent", executionRep); + fail("Not expected to update execution with not-existent flow"); + } catch (NotFoundException nfe) { + // Expected + } + + // Update execution with not-existent ID - SHOULD FAIL + AuthenticationExecutionInfoRepresentation executionRep2 = new AuthenticationExecutionInfoRepresentation(); + executionRep2.setId("not-existent"); + try { + authMgmtResource.updateExecutions("new-client-flow", executionRep2); + fail("Not expected to update not-existent execution"); + } catch (NotFoundException nfe) { + // Expected + } + + // Update success + executionRep.setRequirement(ALTERNATIVE); + authMgmtResource.updateExecutions("new-client-flow", executionRep); + + // Check updated + executionRep = findExecutionByProvider("client-secret", authMgmtResource.getExecutions("new-client-flow")); + assertEquals(ALTERNATIVE, executionRep.getRequirement()); + + // Remove execution with not-existent ID + try { + authMgmtResource.removeExecution("not-existent"); + fail("Didn't expect to find execution"); + } catch (NotFoundException nfe) { + // Expected + } + + // Successfuly remove execution and flow + authMgmtResource.removeExecution(executionRep.getId()); + + AuthenticationFlowRepresentation rep = findFlowByAlias("new-client-flow", authMgmtResource.getFlows()); + authMgmtResource.deleteFlow(rep.getId()); + } + + @Test + public void testRequirementsInExecution() { + HashMap params = new HashMap<>(); + String newBrowserFlow = "new-exec-flow"; + + params.put("newName", newBrowserFlow); + try (Response response = authMgmtResource.copy("browser", params)) { + assertEquals( 201, response.getStatus(), "Copy flow"); + } + + addExecutionCheckReq(newBrowserFlow, "auth-username-form", params, REQUIRED); + addExecutionCheckReq(newBrowserFlow, "webauthn-authenticator", params, DISABLED); + + AuthenticationFlowRepresentation rep = findFlowByAlias(newBrowserFlow, authMgmtResource.getFlows()); + assertNotNull(rep); + authMgmtResource.deleteFlow(rep.getId()); + } + + private void addExecutionCheckReq(String flow, String providerID, HashMap params, String expectedRequirement) { + params.put("provider", providerID); + authMgmtResource.addExecution(flow, params); + + List executionReps = authMgmtResource.getExecutions(flow); + AuthenticationExecutionInfoRepresentation exec = findExecutionByProvider(providerID, executionReps); + + assertNotNull(exec); + assertEquals(expectedRequirement, exec.getRequirement()); + + authMgmtResource.removeExecution(exec.getId()); + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/FlowTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/FlowTest.java new file mode 100644 index 0000000..0c82db7 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/FlowTest.java @@ -0,0 +1,781 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.InternalServerErrorException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; + +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.common.util.StreamUtil; +import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation; +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.OAuth2ErrorRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; + +import org.keycloak.testsuite.util.ApiUtil; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.testcontainers.shaded.org.hamcrest.MatcherAssert.assertThat; +import static org.testcontainers.shaded.org.hamcrest.core.StringContains.containsString; + +/** + * @author Marko Strukelj + */ +public class FlowTest extends AbstractAuthenticationTest { + + // KEYCLOAK-3681: Delete top flow doesn't delete all subflows + @Test + public void testRemoveSubflows() { + createFlow(newFlow("Foo", "Foo flow", "generic", true, false)); + addFlowToParent("Foo", "child"); + addFlowToParent("child", "grandchild"); + + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation found = findFlowByAlias("Foo", flows); + authMgmtResource.deleteFlow(found.getId()); + + createFlow(newFlow("Foo", "Foo flow", "generic", true, false)); + addFlowToParent("Foo", "child"); + + // Under the old code, this would throw an error because "grandchild" + // was left in the database + addFlowToParent("child", "grandchild"); + + authMgmtResource.deleteFlow(findFlowByAlias("Foo", authMgmtResource.getFlows()).getId()); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void testRemoveExecutionSubflow() { + createFlow(newFlow("Foo", "Foo flow", "generic", true, false)); + addFlowToParent("Foo", "child"); + addFlowToParent("child", "grandchild"); + + // remove the foo child but using the execution + List fooExecutions = authMgmtResource.getExecutions("Foo"); + AuthenticationExecutionInfoRepresentation childExececution = fooExecutions.stream() + .filter(r -> "child".equals(r.getDisplayName()) && r.getLevel() == 0).findAny().orElse(null); + assertNotNull(childExececution); + authMgmtResource.removeExecution(childExececution.getId()); + + // check subflows were removed and can be re-created + addFlowToParent("Foo", "child"); + addFlowToParent("child", "grandchild"); + + authMgmtResource.deleteFlow(findFlowByAlias("Foo", authMgmtResource.getFlows()).getId()); + } + + private void addFlowToParent(String parentAlias, String childAlias) { + Map data = new HashMap<>(); + data.put("alias", childAlias); + data.put("type", "generic"); + data.put("description", childAlias + " flow"); + authMgmtResource.addExecutionFlow(parentAlias, data); + } + + @Test + public void testAddFlowWithRestrictedCharInAlias() { + Response resp = authMgmtResource.createFlow(newFlow("fo]o", "Browser flow", "basic-flow", true, false)); + assertEquals(400, resp.getStatus()); + } + + @Test + public void testAddRemoveFlow() { + + // test that built-in flow cannot be deleted + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation builtInFlow = flows.stream().filter(AuthenticationFlowRepresentation::isBuiltIn).findAny().orElse(null); + assertNotNull(builtInFlow, "No built in flow in the realm"); + try { + authMgmtResource.deleteFlow(builtInFlow.getId()); + fail("deleteFlow should fail for built in flow"); + } catch (BadRequestException e) { + OAuth2ErrorRepresentation error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class); + assertEquals("Can't delete built in flow", error.getError()); + } + + // try create new flow using alias of already existing flow + Response response = authMgmtResource.createFlow(newFlow("browser", "Browser flow", "basic-flow", true, false)); + try { + assertEquals(409, response.getStatus(), "createFlow using the alias of existing flow should fail"); + } finally { + response.close(); + } + + // try create flow without alias + response = authMgmtResource.createFlow(newFlow(null, "Browser flow", "basic-flow", true, false)); + try { + assertEquals(409, response.getStatus(), "createFlow using the alias of existing flow should fail"); + } finally { + response.close(); + } + + + // create new flow that should succeed + AuthenticationFlowRepresentation newFlow = newFlow("browser-2", "Browser flow", "basic-flow", true, false); + createFlow(newFlow); + + // check that new flow is returned in a children list + flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation found = findFlowByAlias("browser-2", flows); + + assertNotNull(found, "created flow visible in parent"); + compareFlows(newFlow, found); + + // check lookup flow with unexistent ID + try { + authMgmtResource.getFlow("id-123-notExistent"); + fail("Not expected to find unexistent flow"); + } catch (NotFoundException nfe) { + // Expected + } + + // check that new flow is returned individually + AuthenticationFlowRepresentation found2 = authMgmtResource.getFlow(found.getId()); + assertNotNull(found2, "created flow visible directly"); + compareFlows(newFlow, found2); + + + // add execution flow to some parent flow + Map data = new HashMap<>(); + data.put("alias", "SomeFlow"); + data.put("type", "basic-flow"); + data.put("description", "Test flow"); + // This tests against a regression in KEYCLOAK-16656 + data.put("provider", "registration-page-form"); + + Map data2 = new HashMap<>(); + data2.put("alias", "SomeFlow2"); + data2.put("type", "form-flow"); + data2.put("description", "Test flow 2"); + data2.put("provider", "registration-page-form"); + + + // inexistent parent flow - should fail + try { + authMgmtResource.addExecutionFlow("inexistent-parent-flow-alias", data); + fail("addExecutionFlow for inexistent parent should have failed"); + } catch (Exception expected) { + // Expected + } + + // already existent flow - should fail + try { + data.put("alias", "browser"); + authMgmtResource.addExecutionFlow("browser-2", data); + fail("addExecutionFlow should have failed as browser flow already exists"); + } catch (Exception expected) { + // Expected + } + + // Successfully add flow + data.put("alias", "SomeFlow"); + authMgmtResource.addExecutionFlow("browser-2", data); + authMgmtResource.addExecutionFlow("browser-2", data2); + + // check that new flow is returned in a children list + flows = authMgmtResource.getFlows(); + found2 = findFlowByAlias("browser-2", flows); + assertNotNull(found2, "created flow visible in parent"); + + List execs = found2.getAuthenticationExecutions(); + assertNotNull(execs); + assertEquals( 2, execs.size()); + + AuthenticationExecutionExportRepresentation expected = new AuthenticationExecutionExportRepresentation(); + expected.setFlowAlias("SomeFlow"); + expected.setUserSetupAllowed(false); + expected.setAuthenticatorFlow(true); + expected.setRequirement("DISABLED"); + expected.setPriority(0); + compareExecution(expected, execs.get(0)); + + expected = new AuthenticationExecutionExportRepresentation(); + expected.setFlowAlias("SomeFlow2"); + expected.setUserSetupAllowed(false); + expected.setAuthenticator("registration-page-form"); + expected.setAuthenticatorFlow(true); + expected.setRequirement("DISABLED"); + expected.setPriority(1); + compareExecution(expected, execs.get(1)); + + // delete non-built-in flow + authMgmtResource.deleteFlow(found.getId()); + + // check the deleted flow is no longer returned + flows = authMgmtResource.getFlows(); + found = findFlowByAlias("browser-2", flows); + assertNull(found, "flow deleted"); + + // Check deleting flow second time will fail + try { + authMgmtResource.deleteFlow("id-123-notExistent"); + fail("Not expected to delete flow, which doesn't exist"); + } catch (NotFoundException nfe) { + // Expected + } + } + + @KeycloakVersion(min = "26.0.0") + @Test + public void testRemoveUsedFlow() { + String flowAlias = "test"; + String flowId = createFlow(newFlow(flowAlias, "Test flow", "generic", true, false)); + Runnable assertRemoveFail = () -> { + try { + authMgmtResource.deleteFlow(flowId); + fail("Not expected to delete flow that is in use."); + } catch (WebApplicationException e) { + OAuth2ErrorRepresentation error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class); + assertThat(error.getErrorDescription(), containsString("For more on this error consult the server log")); + } + }; + + { + // used in realm flow + RealmRepresentation realm = realmResource.toRepresentation(); + BiConsumer, Consumer> assertRemoveFailInRealm = + (rollbackFlow, updateFlow) -> { + String rollbackValue = rollbackFlow.get(); + try { + updateFlow.accept(flowAlias); + realmResource.update(realm); + + assertRemoveFail.run(); + } finally { + updateFlow.accept(rollbackValue); + realmResource.update(realm); + } + }; + assertRemoveFailInRealm.accept(realm::getBrowserFlow, realm::setBrowserFlow); + assertRemoveFailInRealm.accept(realm::getRegistrationFlow, realm::setRegistrationFlow); + assertRemoveFailInRealm.accept(realm::getClientAuthenticationFlow, realm::setClientAuthenticationFlow); + assertRemoveFailInRealm.accept(realm::getDirectGrantFlow, realm::setDirectGrantFlow); + assertRemoveFailInRealm.accept(realm::getResetCredentialsFlow, realm::setResetCredentialsFlow); + assertRemoveFailInRealm.accept(realm::getDockerAuthenticationFlow, realm::setDockerAuthenticationFlow); + assertRemoveFailInRealm.accept(realm::getFirstBrokerLoginFlow, realm::setFirstBrokerLoginFlow); + } + + { + // used by client override + ClientRepresentation client = realmResource.clients().findByClientId("account").get(0); + ClientResource clientResource = realmResource.clients().get(client.getId()); + + Map map = new HashMap<>(); + map.put("browser", flowId); + try { + client.setAuthenticationFlowBindingOverrides(map); + clientResource.update(client); + + assertRemoveFail.run(); + } finally { + map.put("browser", ""); + client.setAuthenticationFlowBindingOverrides(map); + clientResource.update(client); + } + } + + { + // used by idp override + IdentityProviderRepresentation idp = new IdentityProviderRepresentation(); + idp.setAlias("idp"); + idp.setProviderId("oidc"); + + Response response = realmResource.identityProviders().create(idp); + assertNotNull(ApiUtil.getCreatedId(response)); + response.close(); + getCleanup("test").addIdentityProviderAlias(idp.getAlias()); + + IdentityProviderResource idpResource = realmResource.identityProviders().get("idp"); + BiConsumer, Consumer> assertRemoveFailByIdp = + (rollbackIdp, updateIdp) -> { + String rollbackValue = rollbackIdp.get(); + try { + updateIdp.accept(flowAlias); + idpResource.update(idp); + + assertRemoveFail.run(); + } finally { + updateIdp.accept(rollbackValue); + idpResource.update(idp); + } + }; + + assertRemoveFailByIdp.accept(idp::getFirstBrokerLoginFlowAlias, idp::setFirstBrokerLoginFlowAlias); + assertRemoveFailByIdp.accept(idp::getPostBrokerLoginFlowAlias, idp::setPostBrokerLoginFlowAlias); + } + } + + @Test + @KeycloakVersion(max = "25.0.6") + public void testCopyFlow() throws IOException { + + HashMap params = new HashMap<>(); + params.put("newName", "clients"); + + // copy using existing alias as new name + Response response = authMgmtResource.copy("browser", params); + try { + assertEquals(response.getStatus(), Status.CONFLICT.getStatusCode(), "Copy flow using the new alias of existing flow should fail"); + String responseString = StreamUtil.readString((InputStream) response.getEntity()); + assertThat("Copy flow using the new alias of existing flow should fail", responseString, containsString("already exists")); + assertThat("Copy flow using the new alias of existing flow should fail", responseString, containsString("flow alias")); + } finally { + response.close(); + } + + // copy non-existing flow + params.clear(); + response = authMgmtResource.copy("non-existent", params); + try { + assertEquals(response.getStatus(), Status.NOT_FOUND.getStatusCode(), "Copy non-existing flow"); + + } finally { + response.close(); + } + + // copy that should succeed + params.put("newName", "Copy of browser"); + response = authMgmtResource.copy("browser", params); + try { + assertEquals(response.getStatus(), Status.CREATED.getStatusCode(), "Copy flow"); + } finally { + response.close(); + } + + // compare original flow with a copy - fields should be the same except id, alias, and builtIn + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation browser = findFlowByAlias("browser", flows); + AuthenticationFlowRepresentation copyOfBrowser = findFlowByAlias("Copy of browser", flows); + + assertNotNull(browser); + assertNotNull(copyOfBrowser); + + // adjust expected values before comparing + browser.setAlias("Copy of browser"); + browser.setBuiltIn(false); + browser.getAuthenticationExecutions().get(3).setFlowAlias("Copy of browser forms"); + compareFlows(browser, copyOfBrowser); + + // get new flow directly and compare + copyOfBrowser = authMgmtResource.getFlow(copyOfBrowser.getId()); + assertNotNull(copyOfBrowser); + compareFlows(browser, copyOfBrowser); + authMgmtResource.deleteFlow(copyOfBrowser.getId()); + } + + @KeycloakVersion(min = "26") + public void testCopyFlowOrg() { + + // compare original flow with a copy - fields should be the same except id, alias, and builtIn + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation browser = findFlowByAlias("browser", flows); + AuthenticationFlowRepresentation copyOfBrowser = findFlowByAlias("Copy of browser", flows); + + assertNotNull(browser); + assertNotNull(copyOfBrowser); + + // adjust expected values before comparing + browser.setAlias("Copy of browser"); + browser.setBuiltIn(false); + browser.getAuthenticationExecutions().get(3).setFlowAlias("Copy of browser Organization"); + browser.getAuthenticationExecutions().get(4).setFlowAlias("Copy of browser forms"); + compareFlows(browser, copyOfBrowser); + + // get new flow directly and compare + copyOfBrowser = authMgmtResource.getFlow(copyOfBrowser.getId()); + assertNotNull(copyOfBrowser); + compareFlows(browser, copyOfBrowser); + authMgmtResource.deleteFlow(copyOfBrowser.getId()); + } + + @Test + // KEYCLOAK-2580 + public void addExecutionFlow() { + HashMap params = new HashMap<>(); + params.put("newName", "parent"); + Response response = authMgmtResource.copy("browser", params); + assertEquals(201, response.getStatus()); + response.close(); + + params = new HashMap<>(); + params.put("alias", "child"); + params.put("description", "Description"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + + authMgmtResource.addExecutionFlow("parent", params); + + authMgmtResource.deleteFlow(findFlowByAlias("parent", authMgmtResource.getFlows()).getId()); + } + + @Test + //KEYCLOAK-12741 + //test editing of authentication flows + public void editFlowTest() { + List flows; + + //copy an existing one first + HashMap params = new HashMap<>(); + params.put("newName", "Copy of browser"); + Response response = authMgmtResource.copy("browser", params); + try { + assertEquals( 201, response.getStatus()); + } finally { + response.close(); + } + + //load the newly copied flow + flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation testFlow = findFlowByAlias("Copy of browser", flows); + //Set a new unique name. Should succeed + testFlow.setAlias("Copy of browser2"); + authMgmtResource.updateFlow(testFlow.getId(), testFlow); + flows = authMgmtResource.getFlows(); + assertEquals("Copy of browser2", findFlowByAlias("Copy of browser2", flows).getAlias()); + + //Create new flow and edit the old one to have the new ones name + AuthenticationFlowRepresentation newFlow = newFlow("New Flow", "Test description", "basic-flow", true, false); + createFlow(newFlow); + // check that new flow is returned in a children list + flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation found = findFlowByAlias("New Flow", flows); + + assertNotNull(found, "created flow visible in parent"); + compareFlows(newFlow, found); + + //try to update old flow with alias that already exists + testFlow.setAlias("New Flow"); + try { + authMgmtResource.updateFlow(found.getId(), testFlow); + } catch (ClientErrorException exception){ + //expoected + } + + //try to update old flow with an alias with illegal characters + testFlow.setAlias("New(Flow"); + try { + authMgmtResource.updateFlow(found.getId(), testFlow); + } catch (ClientErrorException exception){ + //expected + } + + flows = authMgmtResource.getFlows(); + + //name should be the same for the old Flow + assertEquals("Copy of browser2", findFlowByAlias("Copy of browser2", flows).getAlias()); + + //Only update the description + found.setDescription("New description"); + authMgmtResource.updateFlow(found.getId(), found); + flows = authMgmtResource.getFlows(); + + assertEquals("New description", findFlowByAlias("New Flow", flows).getDescription()); + + //Update name and description + found.setAlias("New Flow2"); + found.setDescription("New description2"); + authMgmtResource.updateFlow(found.getId(), found); + flows = authMgmtResource.getFlows(); + + assertEquals("New Flow2", findFlowByAlias("New Flow2", flows).getAlias()); + assertEquals("New description2", findFlowByAlias("New Flow2", flows).getDescription()); + assertNull(findFlowByAlias("New Flow", flows)); + + authMgmtResource.deleteFlow(testFlow.getId()); + authMgmtResource.deleteFlow(found.getId()); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void editExecutionFlowTest() { + HashMap params = new HashMap<>(); + List executionReps; + //create new parent flow + AuthenticationFlowRepresentation newFlow = newFlow("Parent-Flow", "This is a parent flow", "basic-flow", true, false); + createFlow(newFlow); + + //create a child sub flow + params.put("alias", "Child-Flow"); + params.put("description", "This is a child flow"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + + authMgmtResource.addExecutionFlow("Parent-Flow", params); + + executionReps = authMgmtResource.getExecutions("Parent-Flow"); + + //create another with the same name of the previous one. Should fail to create + params = new HashMap<>(); + params.put("alias", "Child-Flow"); + params.put("description", "This is another child flow"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + + try { + authMgmtResource.addExecutionFlow("Parent-Flow", params); + fail("addExecutionFlow the alias already exist"); + } catch (Exception expected) { + // Expected + } + + AuthenticationExecutionInfoRepresentation found = executionReps.get(0); + found.setDisplayName("Parent-Flow"); + + try { + authMgmtResource.updateExecutions("Parent-Flow", found); + } catch (ClientErrorException exception){ + //expected + } + + //edit both name and description + found.setDisplayName("Child-Flow2"); + found.setDescription("This is another child flow2"); + + authMgmtResource.updateExecutions("Parent-Flow", found); + executionReps = authMgmtResource.getExecutions("Parent-Flow"); + assertEquals("Child-Flow2", executionReps.get(0).getDisplayName()); + assertEquals("This is another child flow2", executionReps.get(0).getDescription()); + + //edit only description + found.setDescription("This is another child flow3"); + authMgmtResource.updateExecutions("Parent-Flow", found); + + executionReps = authMgmtResource.getExecutions("Parent-Flow"); + assertEquals("Child-Flow2", executionReps.get(0).getDisplayName()); + assertEquals("This is another child flow3", executionReps.get(0).getDescription()); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void prioritySetTest() { + //create new parent flow + AuthenticationFlowRepresentation newFlow = newFlow("Parent-Flow", "This is a parent flow", "basic-flow", true, false); + createFlow(newFlow); + + HashMap params = new HashMap<>(); + params.put("alias", "Child-Flow1"); + params.put("description", "This is a child flow"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + params.put("priority", 50); + + authMgmtResource.addExecutionFlow("Parent-Flow", params); + + params.clear(); + params.put("alias", "Child-Flow2"); + params.put("description", "This is a second child flow"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + params.put("priority", 10); + + authMgmtResource.addExecutionFlow("Parent-Flow", params); + + params.clear(); + params.put("alias", "Child-Flow3"); + params.put("description", "This is a third child flow"); + params.put("provider", "registration-page-form"); + params.put("type", "basic-flow"); + params.put("priority", 20); + + authMgmtResource.addExecutionFlow("Parent-Flow", params); + + List executionReps = authMgmtResource.getExecutions("Parent-Flow"); + // Verify the initial order and priority value + assertEquals("Child-Flow2", executionReps.get(0).getDisplayName()); + assertEquals(10, executionReps.get(0).getPriority()); + assertEquals("Child-Flow3", executionReps.get(1).getDisplayName()); + assertEquals(20, executionReps.get(1).getPriority()); + assertEquals("Child-Flow1", executionReps.get(2).getDisplayName()); + assertEquals(50, executionReps.get(2).getPriority()); + + // Move last execution to the beginning + AuthenticationExecutionInfoRepresentation lastToFirst = executionReps.get(2); + lastToFirst.setPriority(5); + authMgmtResource.updateExecutions("Parent-Flow", lastToFirst); + executionReps = authMgmtResource.getExecutions("Parent-Flow"); + + // Verify new order and priority + assertEquals("Child-Flow1", executionReps.get(0).getDisplayName()); + assertEquals(5, executionReps.get(0).getPriority()); + assertEquals("Child-Flow2", executionReps.get(1).getDisplayName()); + assertEquals(10, executionReps.get(1).getPriority()); + assertEquals("Child-Flow3", executionReps.get(2).getDisplayName()); + assertEquals(20, executionReps.get(2).getPriority()); + } + + @Test + public void failWithLongDescription() throws IOException { + AuthenticationFlowRepresentation rep = authMgmtResource.getFlows().stream() + .filter(new Predicate() { + @Override + public boolean test(AuthenticationFlowRepresentation rep) { + return "docker auth".equals(rep.getAlias()); + } + }).findAny().orElse(null); + + assertNotNull(rep); + + StringBuilder name = new StringBuilder(); + + while (name.length() < 300) { + name.append("invalid"); + } + + rep.setDescription(name.toString()); + + try { + authMgmtResource.updateFlow(rep.getId(), rep); + fail("Should fail because the description is too long"); + } catch (InternalServerErrorException isee) { + try (Response response = isee.getResponse()) { + assertEquals(500, response.getStatus()); + assertFalse(StreamUtil.readString((InputStream) response.getEntity(), Charset.forName("UTF-8")).toLowerCase().contains("exception")); + } + } catch (Exception e) { + fail("Unexpected exception"); + } + } + + @Test + public void testAddRemoveExecutionsFailInBuiltinFlow() throws IOException { + // get a built in flow + List flows = authMgmtResource.getFlows(); + AuthenticationFlowRepresentation flow = flows.stream().filter(AuthenticationFlowRepresentation::isBuiltIn).findFirst().orElse(null); + assertNotNull(flow, "There is no builtin flow"); + + // adding an execution should fail + Map data = new HashMap<>(); + data.put("provider", "allow-access-authenticator"); + BadRequestException e = assertThrows(BadRequestException.class, () -> authMgmtResource.addExecution(flow.getAlias(), data)); + OAuth2ErrorRepresentation error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class); + assertEquals("It is illegal to add execution to a built in flow", error.getError()); + + // adding a sub-flow should fail as well + e = assertThrows(BadRequestException.class, () -> addFlowToParent(flow.getAlias(), "child")); + error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class); + assertEquals("It is illegal to add sub-flow to a built in flow", error.getError()); + + // removing any execution (execution or flow) should fail too + List executions = authMgmtResource.getExecutions(flow.getAlias()); + assertNotNull(executions, "The builtin flow has no executions"); + assertFalse(executions.isEmpty(), "The builtin flow has no executions"); + e = assertThrows(BadRequestException.class, () -> authMgmtResource.removeExecution(executions.get(0).getId())); + error = e.getResponse().readEntity(OAuth2ErrorRepresentation.class); + assertEquals("It is illegal to remove execution from a built in flow", error.getError()); + } + + @Test + public void testExecutionConfigDuplicated() { + AuthenticationFlowRepresentation existingFlow = null; + + for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) { + if (flow.getAlias().equals("browser")) { + existingFlow = flow; + } + } + + assertNotNull(existingFlow); + + List executions = authMgmtResource.getExecutions(existingFlow.getAlias()); + AuthenticationExecutionInfoRepresentation executionWithConfig = null; + + for (AuthenticationExecutionInfoRepresentation execution : executions) { + if ("identity-provider-redirector".equals(execution.getProviderId())) { + executionWithConfig = execution; + } + } + + assertNotNull(executionWithConfig); + + AuthenticatorConfigRepresentation executionConfig = new AuthenticatorConfigRepresentation(); + + executionConfig.setAlias("test-execution-config"); + + Map map = new HashMap<>(); + map.put("key", "value"); + executionConfig.setConfig(map); + + try (Response response = authMgmtResource.newExecutionConfig(executionWithConfig.getId(), executionConfig)) { + getCleanup("test").addAuthenticationConfigId(ApiUtil.getCreatedId(response)); + } + + String newFlowName = "Duplicated of " + "browser"; + Map copyFlowParams = new HashMap<>(); + copyFlowParams.put("newName", newFlowName); + authMgmtResource.copy(existingFlow.getAlias(), copyFlowParams).close(); + + AuthenticationFlowRepresentation newFlow = null; + + for (AuthenticationFlowRepresentation flow : authMgmtResource.getFlows()) { + if (flow.getAlias().equals(newFlowName)) { + newFlow = flow; + } + } + + Set existingExecutionConfigIds = authMgmtResource.getExecutions(existingFlow.getAlias()) + .stream().map(AuthenticationExecutionInfoRepresentation::getAuthenticationConfig) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + assertFalse(existingExecutionConfigIds.isEmpty()); + + Set newExecutionConfigIds = authMgmtResource.getExecutions(newFlow.getAlias()) + .stream().map(AuthenticationExecutionInfoRepresentation::getAuthenticationConfig) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + assertFalse(newExecutionConfigIds.isEmpty()); + + for (String executionConfigId : newExecutionConfigIds) { + assertFalse(existingExecutionConfigIds.contains(executionConfigId), "Execution config not duplicated"); + } + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ProvidersTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ProvidersTest.java new file mode 100644 index 0000000..9cbcec0 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ProvidersTest.java @@ -0,0 +1,272 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.Test; +import org.keycloak.client.testsuite.Assert; +import org.keycloak.client.testsuite.framework.KeycloakVersion; +import org.keycloak.common.Profile; +import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation; +import org.keycloak.representations.idm.ConfigPropertyRepresentation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Marko Strukelj + */ +public class ProvidersTest extends AbstractAuthenticationTest { + + @Test + public void testFormProviders() { + List> result = authMgmtResource.getFormProviders(); + + assertNotNull(result, "null result"); + assertEquals(1, result.size(), "size"); + Map item = result.get(0); + + assertEquals("registration-page-form", item.get("id")); + assertEquals("Registration Page", item.get("displayName")); + assertEquals("This is the controller for the registration page", item.get("description")); + } + + @KeycloakVersion(min = "25.0.0") + @Test + public void testFormActionProviders() { + List> result = authMgmtResource.getFormActionProviders(); + + List> expected = new LinkedList<>(); + addProviderInfo(expected, "registration-recaptcha-action", "reCAPTCHA", "Adds Google reCAPTCHA to the form."); + addProviderInfo(expected, "registration-recaptcha-enterprise", "reCAPTCHA Enterprise", "Adds Google reCAPTCHA Enterprise to the form."); + addProviderInfo(expected, "registration-password-action", "Password Validation", + "Validates that password matches password confirmation field. It also will store password in user's credential store."); + addProviderInfo(expected, "registration-user-creation", "Registration User Profile Creation", + "This action must always be first! Validates the username and user profile of the user in validation phase. " + + "In success phase, this will create the user in the database including his user profile."); + addProviderInfo(expected, "registration-terms-and-conditions", "Terms and conditions", + "Asks the user to accept terms and conditions before submitting its registration form."); + + compareProviders(expected, result); + } + + @Test + public void testClientAuthenticatorProviders() { + List> result = authMgmtResource.getClientAuthenticatorProviders(); + + List> expected = new LinkedList<>(); + addClientAuthenticatorProviderInfo(expected, "client-jwt", "Signed Jwt", + "Validates client based on signed JWT issued by client and signed with the Client private key", false); + addClientAuthenticatorProviderInfo(expected, "client-secret", "Client Id and Secret", "Validates client based on 'client_id' and " + + "'client_secret' sent either in request parameters or in 'Authorization: Basic' header", true); + addClientAuthenticatorProviderInfo(expected, "client-x509", "X509 Certificate", + "Validates client based on a X509 Certificate", false); + addClientAuthenticatorProviderInfo(expected, "client-secret-jwt", "Signed Jwt with Client Secret", + "Validates client based on signed JWT issued by client and signed with the Client Secret", true); + + compareProviders(expected, result); + } + + @Test + public void testPerClientConfigDescriptions() { + Map> configs = authMgmtResource.getPerClientConfigDescription(); + assertTrue(configs.containsKey("client-jwt")); + assertTrue(configs.containsKey("client-secret")); + assertTrue(configs.get("client-jwt").isEmpty()); + assertTrue(configs.get("client-secret").isEmpty()); + } + + @Test + public void testAuthenticatorConfigDescription() { + // Try some not-existent provider + try { + authMgmtResource.getAuthenticatorConfigDescription("not-existent"); + fail("Don't expected to find provider 'not-existent'"); + } catch (NotFoundException nfe) { + // Expected + } + + AuthenticatorConfigInfoRepresentation infoRep = authMgmtResource.getAuthenticatorConfigDescription("idp-create-user-if-unique"); + assertEquals("Create User If Unique", infoRep.getName()); + assertEquals("idp-create-user-if-unique", infoRep.getProviderId()); + assertEquals("Detect if there is existing Keycloak account with same email like identity provider. If no, create new user", infoRep.getHelpText()); + assertEquals(1, infoRep.getProperties().size()); + Assert.assertProviderConfigProperty(infoRep.getProperties().get(0), "require.password.update.after.registration", "Require Password Update After Registration", + null, "If this option is true and new user is successfully imported from Identity Provider to Keycloak (there is no duplicated email or username detected in Keycloak DB), then this user is required to update his password", + "boolean"); + } + + + @Test + @KeycloakVersion(min = "26.0.0") + public void testInitialAuthenticationProvidersOrg() { + List> providers = authMgmtResource.getAuthenticatorProviders(); + compareProviders(expectedAuthProvidersOrg(), providers); + } + + @Test + @KeycloakVersion(min = "25.0.0", max = "25.1") + public void testInitialAuthenticationProviders() { + List> providers = authMgmtResource.getAuthenticatorProviders(); + compareProviders(expectedAuthProviders25(), providers); + } + + @Test + @KeycloakVersion(min = "24.0.0", max = "24.1") + public void testInitialAuthenticationProviders24() { + List> providers = authMgmtResource.getAuthenticatorProviders(); + compareProviders(expectedAuthProviders(), providers); + } + + private List> expectedAuthProviders() { + ArrayList> result = new ArrayList<>(); + addProviderInfo(result, "auth-conditional-otp-form", "Conditional OTP Form", + "Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions."); + addProviderInfo(result, "auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server."); + addProviderInfo(result, "auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form."); + + String kerberosHelpMessage = "Initiates the SPNEGO protocol. Most often used with Kerberos."; + addProviderInfo(result, "auth-spnego", "Kerberos", kerberosHelpMessage); + addProviderInfo(result, "auth-username-password-form", "Username Password Form", + "Validates a username and password from login form."); + addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form", + "Validates username and password from X509 client certificate received as a part of mutual SSL handshake."); + addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username", + "Validates username and password from X509 client certificate received as a part of mutual SSL handshake."); + addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request"); + addProviderInfo(result, "direct-grant-validate-password", "Password", + "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, "docker-http-basic-authenticator", "Docker Authenticator", "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure"); + 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-auto-link", "Automatically set existing user", "Automatically set existing user to authentication context without any verification"); + addProviderInfo(result, "idp-confirm-link", "Confirm link existing account", "Show the form where user confirms if he wants " + + "to link identity provider with existing account or rather edit user profile data retrieved from identity provider to avoid conflict"); + addProviderInfo(result, "idp-create-user-if-unique", "Create User If Unique", "Detect if there is existing Keycloak account " + + "with same email like identity provider. If no, create new user"); + addProviderInfo(result, "idp-email-verification", "Verify existing account by Email", "Email verification of existing Keycloak " + + "user, that wants to link his user account with identity provider"); + addProviderInfo(result, "idp-review-profile", "Review Profile", + "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 may be already known from identity provider authentication"); + 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", "Removes existing OTP configurations (if chosen) and sets the 'Configure OTP' required action."); + addProviderInfo(result, "reset-password", "Reset Password", "Sets the Update Password required action if execution is REQUIRED. " + + "Will also set it if execution is OPTIONAL and the password is currently configured for it."); + addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication"); + addProviderInfo(result, "webauthn-authenticator-passwordless", "WebAuthn Passwordless Authenticator", "Authenticator for Passwordless WebAuthn authentication"); + + addProviderInfo(result, "auth-username-form", "Username Form", + "Selects a user from his username."); + addProviderInfo(result, "auth-password-form", "Password Form", + "Validates a password from login form."); + addProviderInfo(result, "conditional-user-role", "Condition - user role", + "Flow is executed only if user has the given role."); + addProviderInfo(result, "conditional-user-configured", "Condition - user configured", + "Executes the current flow only if authenticators are configured"); + addProviderInfo(result, "conditional-user-attribute", "Condition - user attribute", + "Flow is executed only if the user attribute exists and has the expected value"); + 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"); + + addProviderInfo(result, "conditional-level-of-authentication", "Condition - Level of Authentication", + "Flow is executed only if the configured LOA or a higher one has been requested but not yet satisfied. After the flow is successfully finished, the LOA in the session will be updated to value prescribed by this condition."); + + addProviderInfo(result, "user-session-limits", "User session count limiter", + "Configures how many concurrent sessions a single user is allowed to create for this realm and/or client"); + + return result; + } + + private List> expectedAuthProviders25() { + List> result = expectedAuthProviders(); + addProviderInfo(result, "idp-confirm-override-link", "Confirm override existing link", "Confirm override the link if there is an existing broker user linked to the account."); + return result; + } + + private List> expectedAuthProvidersOrg() { + List> result = expectedAuthProviders25(); + addProviderInfo(result, "idp-add-organization-member", "Organization Member Onboard", "Adds a federated user as a member of an organization"); + addProviderInfo(result, "organization", "Organization Identity-First Login", "If organizations are enabled, automatically redirects users to the corresponding identity provider."); + return result; + } + + private List> sortProviders(List> providers) { + ArrayList> sorted = new ArrayList<>(providers); + Collections.sort(sorted, new ProviderComparator()); + return sorted; + } + + private void compareProviders(List> expected, List> actual) { + assertEquals(expected.size(), actual.size(), "Providers count"); + // compare ignoring list and map impl types + assertEquals(normalizeResults(actual), normalizeResults(expected)); + } + + private List> normalizeResults(List> list) { + ArrayList> result = new ArrayList<>(); + for (Map item: list) { + result.add(new HashMap<>(item)); + } + return sortProviders(result); + } + + private void addProviderInfo(List> list, String id, String displayName, String description) { + HashMap item = new HashMap<>(); + item.put("id", id); + item.put("displayName", displayName); + item.put("description", description); + list.add(item); + } + + private void addClientAuthenticatorProviderInfo(List> list, String id, String displayName, String description, boolean supportsSecret) { + HashMap item = new HashMap<>(); + item.put("id", id); + item.put("displayName", displayName); + item.put("description", description); + item.put("supportsSecret", supportsSecret); + list.add(item); + } + + private static class ProviderComparator implements Comparator> { + @Override + public int compare(Map o1, Map o2) { + return String.valueOf(o1.get("id")).compareTo(String.valueOf(o2.get("id"))); + } + + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RegistrationFlowTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RegistrationFlowTest.java new file mode 100644 index 0000000..8932ccc --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RegistrationFlowTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.BadRequestException; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Marek Posolda + */ +public class RegistrationFlowTest extends AbstractAuthenticationTest { + + @Test + public void testAddExecution() { + // Add registration flow 2 + AuthenticationFlowRepresentation flowRep = newFlow("registration2", "RegistrationFlow2", "basic-flow", true, false); + createFlow(flowRep); + + // add registration execution form flow + Map data = new HashMap<>(); + data.put("alias", "registrationForm2"); + data.put("type", "form-flow"); + data.put("description", "registrationForm2 flow"); + data.put("provider", "registration-page-form"); + authMgmtResource.addExecutionFlow("registration2", data); + + // Should fail to add execution under top level flow + Map data2 = new HashMap<>(); + data2.put("provider", "registration-password-action"); + try { + authMgmtResource.addExecution("registration2", data2); + fail("Not expected to add execution of type 'registration-password-action' under top flow"); + } catch (BadRequestException bre) { + } + + // Should success to add execution under form flow + authMgmtResource.addExecution("registrationForm2", data2); + } + + // TODO: More type-safety instead of passing generic maps + +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RequiredActionsTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RequiredActionsTest.java new file mode 100644 index 0000000..7109e8c --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/RequiredActionsTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.NotFoundException; + +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.RequiredActionConfigInfoRepresentation; +import org.keycloak.representations.idm.RequiredActionConfigRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; +import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/** + * @author Marko Strukelj + */ +public class RequiredActionsTest extends AbstractAuthenticationTest { + + @Test + public void testRequiredActions() { + List result = authMgmtResource.getRequiredActions(); + + List expected = new ArrayList<>(); + addRequiredAction(expected, "CONFIGURE_TOTP", "Configure OTP", true, false, null); + addRequiredAction(expected, "TERMS_AND_CONDITIONS", "Terms and Conditions", false, false, null); + addRequiredAction(expected, "UPDATE_PASSWORD", "Update Password", true, false, null); + addRequiredAction(expected, "UPDATE_PROFILE", "Update Profile", true, false, null); + addRequiredAction(expected, "VERIFY_EMAIL", "Verify Email", true, false, null); + addRequiredAction(expected, "VERIFY_PROFILE", "Verify Profile", false, false, null); + addRequiredAction(expected, "delete_account", "Delete Account", false, false, null); + addRequiredAction(expected, "delete_credential", "Delete Credential", true, false, null); + addRequiredAction(expected, "update_user_locale", "Update User Locale", true, false, null); + addRequiredAction(expected, "webauthn-register", "Webauthn Register", true, false, null); + addRequiredAction(expected, "webauthn-register-passwordless", "Webauthn Register Passwordless", true, false, null); + + compareRequiredActions(expected, sort(result)); + + RequiredActionProviderRepresentation forUpdate = newRequiredAction("VERIFY_EMAIL", "Verify Email", false, false, null); + authMgmtResource.updateRequiredAction(forUpdate.getAlias(), forUpdate); + + result = authMgmtResource.getRequiredActions(); + RequiredActionProviderRepresentation updated = findRequiredActionByAlias(forUpdate.getAlias(), result); + + assertNotNull(updated, "Required Action still there"); + compareRequiredAction(forUpdate, updated); + + forUpdate.setConfig(Collections.emptyMap()); + authMgmtResource.updateRequiredAction(forUpdate.getAlias(), forUpdate); + + result = authMgmtResource.getRequiredActions(); + updated = findRequiredActionByAlias(forUpdate.getAlias(), result); + + assertNotNull(updated, "Required Action still there"); + compareRequiredAction(forUpdate, updated); + } + + private RequiredActionProviderRepresentation findRequiredActionByAlias(String alias, List list) { + for (RequiredActionProviderRepresentation a: list) { + if (alias.equals(a.getAlias())) { + return a; + } + } + return null; + } + + private List sort(List list) { + ArrayList sorted = new ArrayList<>(list); + Collections.sort(sorted, new RequiredActionProviderComparator()); + return sorted; + } + + private void compareRequiredActions(List expected, List actual) { + assertNotNull(actual, "Actual null"); + assertEquals(expected.size(), actual.size(), "Required actions count"); + + Iterator ite = expected.iterator(); + Iterator ita = actual.iterator(); + while (ite.hasNext()) { + compareRequiredAction(ite.next(), ita.next()); + } + } + + private void compareRequiredAction(RequiredActionProviderRepresentation expected, RequiredActionProviderRepresentation actual) { + assertEquals(expected.getAlias(), actual.getAlias(), "alias - " + expected.getAlias()); + assertEquals(expected.getName(), actual.getName(), "name - " + expected.getAlias()); + assertEquals(expected.isEnabled(), actual.isEnabled(), "enabled - " + expected.getAlias()); + assertEquals(expected.isDefaultAction(), actual.isDefaultAction(), "defaultAction - " + expected.getAlias()); + assertEquals(expected.getConfig() != null ? expected.getConfig() : Collections.emptyMap(), actual.getConfig(), "config - " + expected.getAlias()); + } + + private void addRequiredAction(List target, String alias, String name, boolean enabled, boolean defaultAction, Map conf) { + target.add(newRequiredAction(alias, name, enabled, defaultAction, conf)); + } + + private RequiredActionProviderRepresentation newRequiredAction(String alias, String name, boolean enabled, boolean defaultAction, Map conf) { + RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation(); + action.setAlias(alias); + action.setName(name); + action.setEnabled(enabled); + action.setDefaultAction(defaultAction); + action.setConfig(conf); + return action; + } + + private static class RequiredActionProviderComparator implements Comparator { + @Override + public int compare(RequiredActionProviderRepresentation o1, RequiredActionProviderRepresentation o2) { + return o1.getAlias().compareTo(o2.getAlias()); + } + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftExecutionTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftExecutionTest.java new file mode 100644 index 0000000..398e478 --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftExecutionTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 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.client.testsuite.authentication; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; + +import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; + +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Marko Strukelj + */ +public class ShiftExecutionTest extends AbstractAuthenticationTest { + + @Test + public void testShiftExecution() { + + // copy built-in flow so we get a new editable flow + HashMap params = new HashMap<>(); + params.put("newName", "Copy of browser"); + Response response = authMgmtResource.copy("browser", params); + try { + assertEquals(201, response.getStatus(), "Copy flow"); + } finally { + response.close(); + } + + // get executions + List executions = authMgmtResource.getExecutions("Copy of browser"); + + AuthenticationExecutionInfoRepresentation last = executions.get(executions.size() - 1); + AuthenticationExecutionInfoRepresentation oneButLast = executions.get(executions.size() - 2); + + // Not possible to raisePriority of not-existent flow + try { + authMgmtResource.raisePriority("not-existent"); + fail("Not expected to raise priority of not existent flow"); + } catch (NotFoundException nfe) { + // Expected + } + + // shift last execution up + authMgmtResource.raisePriority(last.getId()); + + List executions2 = authMgmtResource.getExecutions("Copy of browser"); + + AuthenticationExecutionInfoRepresentation last2 = executions2.get(executions.size() - 1); + AuthenticationExecutionInfoRepresentation oneButLast2 = executions2.get(executions.size() - 2); + + assertEquals(last.getId(), oneButLast2.getId(), "Execution shifted up - N"); + assertEquals(oneButLast.getId(), last2.getId(), "Execution shifted up - N-1"); + + // Not possible to lowerPriority of not-existent flow + try { + authMgmtResource.lowerPriority("not-existent"); + fail("Not expected to raise priority of not existent flow"); + } catch (NotFoundException nfe) { + // Expected + } + + // shift one before last down + authMgmtResource.lowerPriority(oneButLast2.getId()); + + executions2 = authMgmtResource.getExecutions("Copy of browser"); + + last2 = executions2.get(executions.size() - 1); + oneButLast2 = executions2.get(executions.size() - 2); + + assertEquals(last.getId(), last2.getId(), "Execution shifted down - N"); + assertEquals(oneButLast.getId(), oneButLast2.getId(), "Execution shifted down - N-1"); + } + + @Test + public void testBuiltinShiftNotAllowed() { + List executions = authMgmtResource.getExecutions("browser"); + + AuthenticationExecutionInfoRepresentation last = executions.get(executions.size() - 1); + AuthenticationExecutionInfoRepresentation oneButLast = executions.get(executions.size() - 2); + + // Not possible to raise - It's builtin flow + try { + authMgmtResource.raisePriority(last.getId()); + fail("Not expected to raise priority of builtin flow"); + } catch (BadRequestException nfe) { + // Expected + } + + // Not possible to lower - It's builtin flow + try { + authMgmtResource.lowerPriority(oneButLast.getId()); + fail("Not expected to lower priority of builtin flow"); + } catch (BadRequestException nfe) { + // Expected + } + + } +} diff --git a/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftRequiredActionTest.java b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftRequiredActionTest.java new file mode 100644 index 0000000..ebfe2ee --- /dev/null +++ b/testsuite/admin-client-tests/src/test/java/org/keycloak/client/testsuite/authentication/ShiftRequiredActionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018 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.client.testsuite.authentication; + +import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.Test; +import org.keycloak.representations.idm.RequiredActionProviderRepresentation; + + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Hiroyuki Wada + */ +public class ShiftRequiredActionTest extends AbstractAuthenticationTest { + + @Test + public void testShiftRequiredAction() { + + // get action + List actions = authMgmtResource.getRequiredActions(); + + RequiredActionProviderRepresentation last = actions.get(actions.size() - 1); + RequiredActionProviderRepresentation oneButLast = actions.get(actions.size() - 2); + + // Not possible to raisePriority of not-existent required action + try { + authMgmtResource.raisePriority("not-existent"); + fail("Not expected to raise priority of not existent required action"); + } catch (NotFoundException nfe) { + // Expected + } + + // shift last required action up + authMgmtResource.raiseRequiredActionPriority(last.getAlias()); + + List actions2 = authMgmtResource.getRequiredActions(); + + RequiredActionProviderRepresentation last2 = actions2.get(actions.size() - 1); + RequiredActionProviderRepresentation oneButLast2 = actions2.get(actions.size() - 2); + + assertEquals(last.getAlias(), oneButLast2.getAlias(), "Required action shifted up - N"); + assertEquals(oneButLast.getAlias(), last2.getAlias(), "Required action up - N-1"); + + // Not possible to lowerPriority of not-existent required action + try { + authMgmtResource.lowerRequiredActionPriority("not-existent"); + fail("Not expected to raise priority of not existent required action"); + } catch (NotFoundException nfe) { + // Expected + } + + // shift one before last down + authMgmtResource.lowerRequiredActionPriority(oneButLast2.getAlias()); + + actions2 = authMgmtResource.getRequiredActions(); + + last2 = actions2.get(actions.size() - 1); + oneButLast2 = actions2.get(actions.size() - 2); + + assertEquals(last.getAlias(), last2.getAlias(), "Required action shifted down - N"); + assertEquals(oneButLast.getAlias(), oneButLast2.getAlias(), "Required action shifted down - N-1"); + } +}