diff --git a/docs/documentation/release_notes/topics/26_1_0.adoc b/docs/documentation/release_notes/topics/26_1_0.adoc
index b0e0dbe6b6d9..623afce4352e 100644
--- a/docs/documentation/release_notes/topics/26_1_0.adoc
+++ b/docs/documentation/release_notes/topics/26_1_0.adoc
@@ -51,6 +51,10 @@ by the LDAP provider.
As OpenShift v3 reached end-of-life a while back, support for identity brokering with OpenShift v3 has been removed from Keycloak.
+= New conditional authenticator `Condition - sub-flow executed`
+
+The `Condition - sub-flow executed` is a new conditional authenticator in {project_name}. The condition checks if a previous sub-flow was executed (or not executed) successfully during the authentication flow execution. For more details, see link:{adminguide_link}#conditions-in-conditional-flows[Conditions in conditional flows].
+
= Defining dependencies between provider factories
When developing extensions for {project_name}, developers can now specify dependencies between provider factories classes by implementing the method `dependsOn()` in the `ProviderFactory` interface.
diff --git a/docs/documentation/server_admin/images/2fa-example1.png b/docs/documentation/server_admin/images/2fa-example1.png
new file mode 100644
index 000000000000..6da90fb4fea8
Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example1.png differ
diff --git a/docs/documentation/server_admin/images/2fa-example2-config.png b/docs/documentation/server_admin/images/2fa-example2-config.png
new file mode 100644
index 000000000000..183c938dd17a
Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example2-config.png differ
diff --git a/docs/documentation/server_admin/images/2fa-example2.png b/docs/documentation/server_admin/images/2fa-example2.png
new file mode 100644
index 000000000000..cc94b424e2a8
Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example2.png differ
diff --git a/docs/documentation/server_admin/images/2fa-example3.png b/docs/documentation/server_admin/images/2fa-example3.png
new file mode 100644
index 000000000000..2b17af38bf31
Binary files /dev/null and b/docs/documentation/server_admin/images/2fa-example3.png differ
diff --git a/docs/documentation/server_admin/topics/authentication/conditions.adoc b/docs/documentation/server_admin/topics/authentication/conditions.adoc
index 7020a20771a1..93cad7201286 100644
--- a/docs/documentation/server_admin/topics/authentication/conditions.adoc
+++ b/docs/documentation/server_admin/topics/authentication/conditions.adoc
@@ -45,6 +45,16 @@ Negate output:::
You can negate the output.
In other words, the attribute should not be present.
+`Condition - sub-flow executed`::
+The condition checks if a previous sub-flow was successfully executed (or not executed) in the authentication process. Therefore, the flow can trigger other steps based on a previous sub-flow termination. These configuration fields exist:
+
+Flow name:::
+The sub-flow name to check if it was executed or not executed. Required.
+
+Check result:::
+When the condition evaluates to true. If `executed` returns true when the configured sub-flow was executed with output success, false otherwise. If `not-executed` returns false when the sub-flow was executed with output success, true otherwise (the negation of the previous option).
+
+
==== Explicitly deny/allow access in conditional flows
You can allow or deny access to resources in a conditional flow.
@@ -85,4 +95,40 @@ The last thing is defining the property with an error message in the login theme
[source]
----
deny-role1 = You do not have required role!
-----
\ No newline at end of file
+----
+
+==== 2FA conditional workflow examples
+
+The section presents some examples of conditional workflows that integrates 2nd Factor Authentication (2FA) in different ways. The examples copy the default `browser` flow and modify the configuration inside the `forms` sub-flow.
+
+===== Conditional 2FA sub-flow
+
+The default `browser` flow uses a `Conditional OTP` sub-flow that already gives a 2FA with OTP Form (One Time Password). Following the same idea, different 2FA methods can be integrated with the `Condition - User Configured`.
+
+.2FA all alternative
+image:images/2fa-example1.png[2FA all alternative]
+
+The `forms` sub-flow contains another `2FA` conditional sub-flow with `Condition - user configured`. Three 2FA steps (OTP, Webauthn and Recovery Codes) are allowed as alternative steps. The user will be able to choose one of the three options, if they are configured for the user. As the sub-flow is conditional, the authentication process will complete successfully if no 2FA credential is configured.
+
+===== Conditional 2FA sub-flow and deny access
+
+The second example continues the previous one. After the `2FA` sub-flow, another flow `Deny access if no 2FA` is used to check if the previous `2FA` was not executed. In that case (the user has no 2FA credential configured) the access is denied.
+
+.2FA all alternative and deny access
+image:images/2fa-example2.png[2FA all alternative and deny access]
+
+The `Condition - sub-flow executed` is configured to detect if the `2FA` sub-flow was not executed previously.
+
+.Configuration for the sub-flow executed
+image:images/2fa-example2-config.png[Configuration for the sub-flow executed]
+
+The step `Deny access` denies the authentication if not executed.
+
+===== Conditional 2FA sub-flow with OTP default
+
+The last example is very similar to the previous one. Instead of denying the access, step `OTP Form` is configured as required.
+
+.2FA all alternative with OTP default
+image:images/2fa-example3.png[2FA all alternative with OTP default]
+
+With this flow, if the user has none of the 2FA methods configured, the OTP setup will be enforced to continue the login.
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticator.java
new file mode 100644
index 000000000000..43514d5d8b67
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticator.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2024 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.conditional;
+
+import java.util.stream.Stream;
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.AuthenticatorConfigModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+/**
+ *
Conditional authenticator to know if a sub-flow was executed successfully in the authentication flow.
+ *
+ * @author rmartinc
+ */
+public class ConditionalSubFlowExecutedAuthenticator implements ConditionalAuthenticator {
+
+ protected static final ConditionalSubFlowExecutedAuthenticator SINGLETON = new ConditionalSubFlowExecutedAuthenticator();
+ private static final Logger logger = Logger.getLogger(ConditionalSubFlowExecutedAuthenticator.class);
+
+ @Override
+ public boolean matchCondition(AuthenticationFlowContext context) {
+ final AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();
+ if (configModel == null || configModel.getConfig() == null) {
+ logger.warnf("No configuration defined for the conditional flow executed. Nothing executed.");
+ return false;
+ }
+
+ final String flowAlias = configModel.getConfig().get(ConditionalSubFlowExecutedAuthenticatorFactory.FLOW_TO_CHECK);
+ final boolean executed = !ConditionalSubFlowExecutedAuthenticatorFactory.CHECK_RESULT_NOT_EXECUTED.equals(
+ configModel.getConfig().get(ConditionalSubFlowExecutedAuthenticatorFactory.CHECK_RESULT));
+ if (flowAlias == null) {
+ logger.warnf("No flow configured in the option '%s'. Nothing executed.", ConditionalSubFlowExecutedAuthenticatorFactory.FLOW_TO_CHECK);
+ return !executed;
+ }
+
+ final RealmModel realm = context.getRealm();
+ final AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
+ if (flow == null) {
+ logger.warnf("No flow '%s' defined in the realm. Nothing executed.", flowAlias);
+ return !executed;
+ }
+
+ final AuthenticationExecutionModel exec = locateExecutionFlowToCheck(realm, context.getTopLevelFlow().getId(), flow.getId());
+ if (exec == null) {
+ logger.warnf("Cannot locate execution for flow '%s' in the top level flow '%s'. Nothing executed.", flowAlias, context.getTopLevelFlow().getAlias());
+ return !executed;
+ }
+
+ AuthenticationSessionModel.ExecutionStatus status = context.getAuthenticationSession().getExecutionStatus().get(exec.getId());
+ logger.tracef("The authentication status for the flow '%s' is %s", flowAlias, status);
+ return executed
+ ? AuthenticationSessionModel.ExecutionStatus.SUCCESS.equals(status)
+ : !AuthenticationSessionModel.ExecutionStatus.SUCCESS.equals(status);
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ // no-op
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ private Stream flattened(RealmModel realm, AuthenticationExecutionModel flowExec) {
+ // flatten the execution model recursively only for flows
+ return Stream.concat(Stream.of(flowExec),
+ realm.getAuthenticationExecutionsStream(flowExec.getFlowId())
+ .filter(AuthenticationExecutionModel::isAuthenticatorFlow)
+ .flatMap(exec -> flattened(realm, exec)));
+ }
+
+ private AuthenticationExecutionModel locateExecutionFlowToCheck(RealmModel realm, String topFlowId, String flowId) {
+ return realm.getAuthenticationExecutionsStream(topFlowId)
+ .filter(AuthenticationExecutionModel::isAuthenticatorFlow)
+ .flatMap(exec -> flattened(realm, exec))
+ .filter(exec -> flowId.equals(exec.getFlowId()))
+ .findAny()
+ .orElse(null);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticatorFactory.java
new file mode 100644
index 000000000000..eeb744dd85fe
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/conditional/ConditionalSubFlowExecutedAuthenticatorFactory.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authentication.authenticators.conditional;
+
+import java.util.List;
+import org.keycloak.Config.Scope;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.provider.ProviderConfigurationBuilder;
+
+/**
+ * Conditional factory to know if a sub-flow was executed successfully in the authentication flow.
+ *
+ * @author rmartinc
+ */
+public class ConditionalSubFlowExecutedAuthenticatorFactory implements ConditionalAuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "conditional-sub-flow-executed";
+ public static final String FLOW_TO_CHECK = "flow_to_check";
+ public static final String CHECK_RESULT = "check_result";
+ public static final String CHECK_RESULT_EXECUTED = "executed";
+ public static final String CHECK_RESULT_NOT_EXECUTED = "not-executed";
+
+ @Override
+ public void init(Scope config) {
+ // no-op
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ // no-op
+ }
+
+ @Override
+ public void close() {
+ // no-op
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Condition - sub-flow executed";
+ }
+
+ @Override
+ public boolean isConfigurable() {
+ return true;
+ }
+
+ @Override
+ public Requirement[] getRequirementChoices() {
+ return new Requirement[]{AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED};
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ @Override
+ public String getHelpText() {
+ return "Condition to evaluate if a sub-flow was executed successfully during the authentication process";
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return ProviderConfigurationBuilder.create()
+ .property()
+ .name(FLOW_TO_CHECK)
+ .type(ProviderConfigProperty.STRING_TYPE)
+ .label("Flow name")
+ .helpText("The sub-flow name to check if it was executed.")
+ .required(true)
+ .add()
+ .property()
+ .name(CHECK_RESULT)
+ .type(ProviderConfigProperty.LIST_TYPE)
+ .label("Check result")
+ .helpText(
+ """
+ When the condition evaluates to true.
+ If 'executed' returns true when the configured sub-flow was executed with output success, false otherwise.
+ If 'not-executed' returns false when the sub-flow was executed with output success, true otherwise.
+ """
+ )
+ .required(true)
+ .options(List.of(CHECK_RESULT_EXECUTED, CHECK_RESULT_NOT_EXECUTED))
+ .defaultValue(CHECK_RESULT_EXECUTED)
+ .add()
+ .build();
+ }
+
+ @Override
+ public ConditionalAuthenticator getSingleton() {
+ return ConditionalSubFlowExecutedAuthenticator.SINGLETON;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index 2f0ff50c6d33..1300f2af6ef3 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -413,8 +413,7 @@ public Response copy(@Parameter(description="name of the existing authentication
data.put("id", copy.getId());
adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri()).representation(data).success();
- return Response.status(Response.Status.CREATED).build();
-
+ return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(copy.getId()).build()).build();
}
public static AuthenticationFlowModel copyFlow(KeycloakSession session, RealmModel realm, AuthenticationFlowModel flow, String newName) {
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 8a15cee7dc60..58e04570281c 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -22,6 +22,7 @@ org.keycloak.authentication.authenticators.browser.PasswordFormFactory
org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticatorFactory
+org.keycloak.authentication.authenticators.conditional.ConditionalSubFlowExecutedAuthenticatorFactory
org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory
org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory
org.keycloak.authentication.authenticators.conditional.ConditionalLoaAuthenticatorFactory
@@ -54,4 +55,4 @@ org.keycloak.authentication.authenticators.access.AllowAccessAuthenticatorFactor
org.keycloak.authentication.authenticators.sessionlimits.UserSessionLimitsAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.RecoveryAuthnCodesFormAuthenticatorFactory
org.keycloak.organization.authentication.authenticators.browser.OrganizationAuthenticatorFactory
-org.keycloak.authentication.authenticators.browser.PasskeysConditionalUIAuthenticatorFactory
\ No newline at end of file
+org.keycloak.authentication.authenticators.browser.PasskeysConditionalUIAuthenticatorFactory
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 37895fa71ee2..861914fe8c9b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -234,6 +234,7 @@ private List