From 1b2de4199e344412d68f21954f94e9976ce969bf Mon Sep 17 00:00:00 2001 From: Felix Dittrich Date: Mon, 15 Jan 2024 18:41:56 +0100 Subject: [PATCH] Add Filter to Distinguish SubFlow Authenticators by config alias --- CHANGELOG.md | 3 + .../repository/ExecutionFlowRepository.java | 12 +- .../service/ImportAuthenticationFlowsIT.java | 56 +++ ...ow_executions_with_same_authenticator.json | 382 ++++++++++++++++++ 4 files changed, 451 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/import-files/auth-flows/33_update_realm__add_multiple_subflow_executions_with_same_authenticator.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c7d73260..8cc597f45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed +- Allow executions of same provider with different configurations in Sub-Auth-Flows + ## [5.10.0] - 2023-12-12 - Updated CI to use Keycloak 23.0.1 - Added correct spelling of "authenticatorFlow" in all import files diff --git a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java index e176152ca..ca6400234 100644 --- a/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java +++ b/src/main/java/de/adorsys/keycloak/config/repository/ExecutionFlowRepository.java @@ -57,7 +57,8 @@ public List getExecutionFlowsByAlias( String topLevelFlowAlias, AuthenticationExecutionExportRepresentation execution) { List executions = searchByAlias( - realmName, topLevelFlowAlias, execution.getAuthenticator(), execution.getFlowAlias()); + realmName, topLevelFlowAlias, execution.getAuthenticator(), + execution.getFlowAlias(), execution.getAuthenticatorConfig()); if (executions.isEmpty()) { String withSubFlow = execution.getFlowAlias() != null @@ -147,11 +148,18 @@ private List searchByAlias( String realmName, String topLevelFlowAlias, String executionProviderId, - String subFlowAlias + String subFlowAlias, + String authenticationConfig ) { return getExecutionsByAuthFlow(realmName, topLevelFlowAlias) .stream() .filter(f -> Objects.equals(f.getProviderId(), executionProviderId)) + .filter(f -> { + if (authenticationConfig != null && f.getAlias() != null) { + return Objects.equals(f.getAlias(), authenticationConfig); + } + return true; + }) .filter(f -> { if (subFlowAlias != null) { return Objects.equals(f.getDisplayName(), subFlowAlias); diff --git a/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java b/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java index 42cb43a09..35d938357 100644 --- a/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java +++ b/src/test/java/de/adorsys/keycloak/config/service/ImportAuthenticationFlowsIT.java @@ -788,6 +788,62 @@ void shouldUpdateMultipleExecutionsWithSameAuthenticatorWithConfig() throws IOEx assertThat(authConfig.get(0).getConfig(), hasEntry(is("defaultProvider"), is("id4"))); } + @Test + @Order(33) + void shouldCreateMultipleSubFlowExecutionsWithSameAuthenticator() throws IOException { + doImport("33_update_realm__add_multiple_subflow_executions_with_same_authenticator.json"); + + RealmRepresentation realm = keycloakProvider.getInstance().realm(REALM_NAME).partialExport(true, true); + + AuthenticationFlowRepresentation topLevelFlow = getAuthenticationFlow(realm, "my top level auth flow"); + assertThat(topLevelFlow.isBuiltIn(), is(false)); + assertThat(topLevelFlow.isTopLevel(), is(true)); + assertThat(topLevelFlow.getAuthenticationExecutions().size(), is(1)); + assertThat(topLevelFlow.getAuthenticationExecutions().get(0).getFlowAlias(), is("my sub auth flow")); + + AuthenticationFlowRepresentation subFlow = getAuthenticationFlow(realm, "my sub auth flow"); + assertThat(subFlow.isBuiltIn(), is(false)); + assertThat(subFlow.isTopLevel(), is(false)); + assertThat(subFlow.getAuthenticationExecutions().size(), is(3)); + + List execution; + execution = getExecutionFromFlow(subFlow, "identity-provider-redirector"); + assertThat(execution, hasSize(2)); + + List executionsId1 = execution.stream() + .filter((config) -> config.getAuthenticatorConfig() != null) + .filter((config) -> config.getAuthenticatorConfig().equals("config-1")) + .collect(Collectors.toList()); + + assertThat(executionsId1, hasSize(1)); + assertThat(executionsId1.get(0).getAuthenticator(), is("identity-provider-redirector")); + assertThat(executionsId1.get(0).getAuthenticatorConfig(), is("config-1")); + assertThat(executionsId1.get(0).getRequirement(), is("ALTERNATIVE")); + + List executionsId2 = execution.stream() + .filter((config) -> config.getAuthenticatorConfig() != null) + .filter((config) -> config.getAuthenticatorConfig().equals("config-2")) + .collect(Collectors.toList()); + + assertThat(executionsId2, hasSize(1)); + assertThat(executionsId2.get(0).getAuthenticator(), is("identity-provider-redirector")); + assertThat(executionsId2.get(0).getAuthenticatorConfig(), is("config-2")); + assertThat(executionsId2.get(0).getRequirement(), is("ALTERNATIVE")); + + assertThat(executionsId2.get(0).getPriority(), greaterThan(executionsId1.get(0).getPriority())); + + List authConfig; + authConfig = getAuthenticatorConfig(realm, "config-1"); + assertThat(authConfig, hasSize(1)); + assertThat(authConfig.get(0).getAlias(), is("config-1")); + assertThat(authConfig.get(0).getConfig(), hasEntry(is("defaultProvider"), is("id1"))); + + authConfig = getAuthenticatorConfig(realm, "config-2"); + assertThat(authConfig, hasSize(1)); + assertThat(authConfig.get(0).getAlias(), is("config-2")); + assertThat(authConfig.get(0).getConfig(), hasEntry(is("defaultProvider"), is("id2"))); + } + @Test @Order(40) void shouldFailWhenTryingToUpdateBuiltInFlow() throws IOException { diff --git a/src/test/resources/import-files/auth-flows/33_update_realm__add_multiple_subflow_executions_with_same_authenticator.json b/src/test/resources/import-files/auth-flows/33_update_realm__add_multiple_subflow_executions_with_same_authenticator.json new file mode 100644 index 000000000..cb31f9841 --- /dev/null +++ b/src/test/resources/import-files/auth-flows/33_update_realm__add_multiple_subflow_executions_with_same_authenticator.json @@ -0,0 +1,382 @@ +{ + "enabled": true, + "realm": "realmWithFlow", + "browserFlow": "my browser", + "directGrantFlow": "my direct grant", + "clientAuthenticationFlow": "my clients", + "dockerAuthenticationFlow": "my docker auth", + "registrationFlow": "my registration", + "resetCredentialsFlow": "my reset credentials", + "authenticationFlows": [ + { + "alias": "my top level auth flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "flowAlias": "my sub auth flow", + "userSetupAllowed": false + }] + }, + { + "alias": "my sub auth flow", + "description": "My auth flow for testing multiple instances of authenticators", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticatorConfig": "config-1", + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 1, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticatorConfig": "config-2", + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 2, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 3, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my registration", + "description": "changed registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 0, + "flowAlias": "my registration form", + "userSetupAllowed": false, + "autheticatorFlow": true, + "authenticatorFlow": true + } + ] + }, + { + "id": "f9037ea2-61c8-4a62-b564-9dfdac233d94", + "alias": "my registration form", + "description": "my registration form with pseudo-id", + "providerId": "form-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 1, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 0, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my reset credentials", + "description": "My changed reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "CONDITIONAL", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my browser", + "description": "My changed browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "my forms", + "userSetupAllowed": false, + "autheticatorFlow": true, + "authenticatorFlow": true + } + ] + }, + { + "alias": "my forms", + "description": "My Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "CONDITIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my direct grant", + "description": "My changed OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "CONDITIONAL", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my clients", + "description": "My changed Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my docker auth", + "description": "My changed Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + }, + { + "alias": "my auth flow with execution-flows", + "description": "My authentication flow with authentication executions", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "my execution-flow", + "userSetupAllowed": false, + "autheticatorFlow": true, + "authenticatorFlow": true + } + ] + }, + { + "alias": "my execution-flow", + "description": "My execution-flow for authentication-flow", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "CONDITIONAL", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false, + "authenticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "alias": "config-1", + "config": { + "defaultProvider": "id1" + } + }, + { + "alias": "config-2", + "config": { + "defaultProvider": "id2" + } + } + ] +}