CVE-2021-3827: Improper Authentication (CWE-287)
value: A flaw was found in keycloak, where the default ECP binding flow allows other authentication flows to be bypassed. By exploiting this behavior, an attacker can bypass the MFA authentication by sending a SOAP request with an AuthnRequest and Authorization header with the user's credentials. The highest threat from this vulnerability is to confidentiality and integrity.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 6.8
Published: 2022-08-23T16:15Z
Repository: https://github.com/keycloak/keycloak
Language: Java
Files changed: services/src/main/java/org/keycloak/protocol/saml/SamlClient.java, services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java, services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java, services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java, services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java, testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java, testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json, testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java, testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java, themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties, themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js, themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
Commit date: 2022-03-31 16:06:44 +0200
Commit message: KEYCLOAK-19177 Disable ECP flow by default for all Saml clients; ecp flow creates only transient users sessions

Diff: diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
index 1b8f23495ee..9cf0cd1847e 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlClient.java
@@ -118,10 +118,18 @@ public class SamlClient extends ClientConfigResolver {
 
     public void setForceNameIDFormat(boolean val) {
         client.setAttribute(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE, Boolean.toString(val));
     }
 
+    public boolean allowECPFlow() {
+        return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW));
+    }
+
+    public void setAllowECPFlow(boolean val) {
+        client.setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, Boolean.toString(val));
+    }
+
     public boolean forceArtifactBinding(){
         return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING));
     }
 
     public void setForceArtifactBinding(boolean val) {
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
index 59f27f50ab3..02a41c48317 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlConfigAttributes.java
@@ -42,6 +42,7 @@ public interface SamlConfigAttributes {
     String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + CertificateInfoHelper.X509CERTIFICATE;
     String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.X509CERTIFICATE;
     String SAML_ENCRYPTION_PRIVATE_KEY_ATTRIBUTE = "saml.encryption." + CertificateInfoHelper.PRIVATE_KEY;
     String SAML_ASSERTION_LIFESPAN = "saml.assertion.lifespan";
     String SAML_ARTIFACT_BINDING_IDENTIFIER = "saml.artifact.binding.identifier";
+    String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";
 }
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java
index fae3e4de091..772885553b9 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolFactory.java
@@ -152,10 +152,14 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
 
         if (rep.getForceNameIDFormat() == null) {
             client.setForceNameIDFormat(false);
         }
 
+        if (rep.getAllowEcpFlow() == null) {
+            client.setAllowECPFlow(false);
+        }
+
         if (rep.getSamlServerSignature() == null) {
             client.setRequiresRealmSignature(true);
         }
         if (rep.getForcePostBinding() == null) {
             client.setForcePostBinding(true);
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java b/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
index a64b847b806..e4da38eee9a 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlRepresentationAttributes.java
@@ -59,10 +59,15 @@ public class SamlRepresentationAttributes {
     public String getForceNameIDFormat() {
         if (getAttributes() == null) return null;
         return getAttributes().get(SamlConfigAttributes.SAML_FORCE_NAME_ID_FORMAT_ATTRIBUTE);
     }
 
+    public String getAllowEcpFlow() {
+        if (getAttributes() == null) return null;
+        return getAttributes().get(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW);
+    }
+
     public String getSamlArtifactBinding() {
         if (getAttributes() == null) return null;
         return getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING);
     }
 
diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
index 7457381d226..f3202a7c3b1 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java
@@ -24,29 +24,30 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.DefaultAuthenticationFlows;
 import org.keycloak.protocol.saml.JaxrsSAML2BindingBuilder;
+import org.keycloak.protocol.saml.SamlClient;
 import org.keycloak.protocol.saml.SamlConfigAttributes;
 import org.keycloak.protocol.saml.SamlProtocol;
 import org.keycloak.protocol.saml.SamlService;
 import org.keycloak.protocol.saml.profile.util.Soap;
 import org.keycloak.saml.SAML2LogoutResponseBuilder;
 import org.keycloak.saml.common.constants.JBossSAMLConstants;
 import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
 import org.keycloak.saml.common.exceptions.ConfigurationException;
 import org.keycloak.saml.common.exceptions.ProcessingException;
 import org.keycloak.saml.validators.DestinationValidator;
+import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.w3c.dom.Document;
 
 import javax.ws.rs.core.Response;
 import javax.xml.soap.SOAPException;
 import javax.xml.soap.SOAPHeaderElement;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Map;
 import java.util.Objects;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
@@ -77,10 +78,16 @@ public class SamlEcpProfileService extends SamlService {
                     return false;
                 }
 
                 @Override
                 protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
+                    // Do not allow ECP login when client does not support it
+                    if (!new SamlClient(client).allowECPFlow()) {
+                        logger.errorf("Client %s is not allowed to execute ECP flow", client.getClientId());
+                        throw new RuntimeException("Client is not allowed to use ECP profile.");
+                    }
+
                     // force passive authentication when executing this profile
                     requestAbstractType.setIsPassive(true);
                     requestAbstractType.setDestination(session.getContext().getUri().getAbsolutePath());
                     return super.loginRequest(relayState, requestAbstractType, client);
                 }
@@ -97,10 +104,12 @@ public class SamlEcpProfileService extends SamlService {
         }
     }
 
     @Override
     protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
+        // Saml ECP flow creates only TRANSIENT user sessions
+        authSession.setClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString());
         return super.newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, createEcpSamlProtocol());
     }
 
     private SamlProtocol createEcpSamlProtocol() {
         return new SamlProtocol() {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java
index 4afbbbbe123..41007f70e96 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/saml/SOAPBindingTest.java
@@ -15,21 +15,38 @@
  * limitations under the License.
  */
 package org.keycloak.testsuite.saml;
 
 import org.junit.Test;
+import org.keycloak.dom.saml.v2.SAML2Object;
+import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
 import org.keycloak.dom.saml.v2.protocol.ResponseType;
 import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.saml.SamlConfigAttributes;
+import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
 import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
 import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
 import org.keycloak.testsuite.util.SamlClientBuilder;
 
+import javax.ws.rs.core.Response;
+import javax.xml.soap.MessageFactory;
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+
+import java.io.IOException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
-import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.nullValue;
+import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
+import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
 import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
 import static org.keycloak.testsuite.util.SamlClient.Binding.SOAP;
 
 public class SOAPBindingTest extends AbstractSamlTest {
 
@@ -212,6 +229,66 @@ public class SOAPBindingTest extends AbstractSamlTest {
                 .executeAndTransform(POST::extractResponse);
 
 
         assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
     }
+
+    @Test
+    public void soapBindingIsNotPossibleForClientsWithSamlEcpFlowAttributeFalse() {
+        // Disable ECP_FLOW_ENABLED switch
+        getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
+                .setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, "false")
+                .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
+                .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
+                .update());
+
+        new SamlClientBuilder()
+                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
+                .basicAuthentication(bburkeUser)
+                .build()
+                .execute(response -> {
+                    assertThat(response, statusCodeIsHC(Response.Status.INTERNAL_SERVER_ERROR));
+
+                    try {
+                        MessageFactory messageFactory = MessageFactory.newInstance();
+                        SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
+                        String faultDetail = soapMessage.getSOAPBody().getFault().getDetail().getValue();
+                        assertThat(faultDetail, is(equalTo("Client is not allowed to use ECP profile.")));
+                    } catch (SOAPException | IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+
+    }
+
+    @Test
+    public void ecpFlowCreatesTransientSessions() {
+        // Disable ECP_FLOW_ENABLED switch
+        getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
+                .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
+                .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
+                .update());
+
+        // Successfully login using ECP flow
+        SAML2Object samlObject = new SamlClientBuilder()
+                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
+                .basicAuthentication(bburkeUser)
+                .build()
+                .executeAndTransform(SOAP::extractResponse).getSamlObject();
+
+        assertThat(samlObject, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
+        ResponseType loginResp1 = (ResponseType) samlObject;
+        AuthnStatementType sessionId = (AuthnStatementType) loginResp1.getAssertions().get(0).getAssertion().getStatements().iterator().next();
+
+        String userSessionId = sessionId.getSessionIndex().split("::")[0];
+
+        // Test that the user session with the given ID does not exist
+        testingClient.server().run(session -> {
+            RealmModel realmByName = session.realms().getRealmByName(REALM_NAME);
+            UserSessionModel userSession = session.sessions().getUserSession(realmByName, userSessionId);
+
+            assertThat(userSession, nullValue());
+        });
+
+
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
index 604c32b39b8..8b4f721ffd0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json
@@ -714,11 +714,12 @@
                 "saml_single_logout_service_url_redirect": "http://localhost:8080/ecp-sp/",
                 "saml.server.signature": "true",
                 "saml.signature.algorithm": "RSA_SHA256",
                 "saml.client.signature": "true",
                 "saml.authnstatement": "true",
-                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
+                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==",
+                "saml.allow.ecp.flow": "true"
             }
         },
         {
             "clientId": "http://localhost:8280/employee-role-mapping/",
             "enabled": true,
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
index 88de5cafb81..97f0fd55f36 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/settings/ClientSettingsForm.java
@@ -2,10 +2,11 @@ package org.keycloak.testsuite.console.page.clients.settings;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import org.keycloak.protocol.saml.SamlConfigAttributes;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.testsuite.console.page.clients.CreateClientForm;
 import org.keycloak.testsuite.console.page.fragment.OnOffSwitch;
 import org.keycloak.testsuite.page.Form;
 import org.keycloak.testsuite.util.Timer;
@@ -260,10 +261,11 @@ public class ClientSettingsForm extends CreateClientForm {
         public static final String SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
         public static final String SAML_ASSERTION_CONSUMER_URL_POST = "saml_assertion_consumer_url_post";
         public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT = "saml_assertion_consumer_url_redirect";
         public static final String SAML_FORCE_NAME_ID_FORMAT = "saml_force_name_id_format";
         public static final String SAML_NAME_ID_FORMAT = "saml_name_id_format";
+        public static final String SAML_ALLOW_ECP_FLOW = SamlConfigAttributes.SAML_ALLOW_ECP_FLOW;
         public static final String SAML_SIGNATURE_CANONICALIZATION_METHOD = "saml_signature_canonicalization_method";
         public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST = "saml_single_logout_service_url_post";
         public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT = "saml_single_logout_service_url_redirect";
 
         @FindBy(xpath = ".//div[@class='onoffswitch' and ./input[@id='samlAuthnStatement']]")
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
index 20c2f2aefd3..86b148a7e89 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/AbstractClientTest.java
@@ -21,14 +21,14 @@ import java.util.Map;
 import static org.junit.Assert.assertEquals;
 import static org.keycloak.testsuite.auth.page.login.OIDCLogin.OIDC;
 import static org.keycloak.testsuite.auth.page.login.OIDCLogin.SAML;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_AUTHNSTATEMENT;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_CLIENT_SIGNATURE;
+import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ALLOW_ECP_FLOW;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_NAME_ID_FORMAT;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_FORCE_POST_BINDING;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_NAME_ID_FORMAT;
-import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ONETIMEUSE_CONDITION;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SERVER_SIGNATURE;
 import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_SIGNATURE_ALGORITHM;
 import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsBooleanAttributes;
 import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsListAttributes;
 import static org.keycloak.testsuite.util.AttributesAssert.assertEqualsStringAttributes;
@@ -87,10 +87,11 @@ public abstract class AbstractClientTest extends AbstractConsoleTest {
         attributes.put(SAML_FORCE_POST_BINDING, "true");
         attributes.put(SAML_SERVER_SIGNATURE, "true");
         attributes.put(SAML_SIGNATURE_ALGORITHM, "RSA_SHA256");
         attributes.put(SAML_FORCE_NAME_ID_FORMAT, "false");
         attributes.put(SAML_NAME_ID_FORMAT, "username");
+        attributes.put(SAML_ALLOW_ECP_FLOW, "false");
         attributes.put(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER, ArtifactBindingUtils.computeArtifactBindingIdentifierString("saml"));
         return attributes;
     }
 
     public ClientRepresentation findClientByClientId(String clientId) {
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 0cd25e66fae..3ead1891e28 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -369,10 +369,12 @@ front-channel-logout-url.tooltip=URL that will cause the client to log itself ou
 front-channel-logout-session-required=Front-Channel Logout Session Required
 front-channel-logout-session-required.tooltip=Specifying whether a sid (session ID) and iss (Issuer) claims are included in the Logout Token when the Front-Channel Logout URL is used.
 
 force-name-id-format=Force Name ID Format
 force-name-id-format.tooltip=Ignore requested NameID subject format and use admin console configured one.
+allow-ecp-flow=Allow ECP Flow
+allow-ecp-flow.tooltip=This client is allowed to use ECP flow for authenticating users.
 name-id-format=Name ID Format
 name-id-format.tooltip=The name ID format to use for the subject.
 mapper.nameid.format.tooltip=Name ID Format using Mapper
 root-url=Root URL
 root-url.tooltip=Root URL appended to relative URLs
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index ce245a5085b..30a7faa65cd 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -1195,10 +1195,11 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
     $scope.samlAssertionSignature = false;
     $scope.samlClientSignature = false;
     $scope.samlEncrypt = false;
     $scope.samlForcePostBinding = false;
     $scope.samlForceNameIdFormat = false;
+    $scope.samlAllowECPFlow = false;
     $scope.samlXmlKeyNameTranformer = $scope.xmlKeyNameTranformers[1];
     $scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
     $scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
     $scope.disableCredentialsTab = client.publicClient;
     $scope.oauth2DeviceAuthorizationGrantEnabled = false;
@@ -1349,10 +1350,17 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
                 $scope.samlForceNameIdFormat = true;
             } else {
                 $scope.samlForceNameIdFormat = false;
             }
         }
+        if ($scope.client.attributes["saml.allow.ecp.flow"]) {
+            if ($scope.client.attributes["saml.allow.ecp.flow"] == "true") {
+                $scope.samlAllowECPFlow = true;
+            } else {
+                $scope.samlAllowECPFlow = false;
+            }
+        }
         if ($scope.client.attributes["saml.multivalued.roles"]) {
             if ($scope.client.attributes["saml.multivalued.roles"] == "true") {
                 $scope.samlMultiValuedRoles = true;
             } else {
                 $scope.samlMultiValuedRoles = false;
@@ -1959,10 +1967,16 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
         if ($scope.samlForceNameIdFormat == true) {
             $scope.clientEdit.attributes["saml_force_name_id_format"] = "true";
         } else {
             $scope.clientEdit.attributes["saml_force_name_id_format"] = "false";
 
+        }
+        if ($scope.samlAllowECPFlow == true) {
+            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "true";
+        } else {
+            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "false";
+
         }
         if ($scope.samlMultiValuedRoles == true) {
             $scope.clientEdit.attributes["saml.multivalued.roles"] = "true";
         } else {
             $scope.clientEdit.attributes["saml.multivalued.roles"] = "false";
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 0b0b8c9a71c..779aacef6ec 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -297,10 +297,17 @@
                 <div class="col-sm-6">
                     <input ng-model="samlForceNameIdFormat" ng-click="switchChange()" name="samlForceNameIdFormat" id="samlForceNameIdFormat" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
                 </div>
                 <kc-tooltip>{{:: 'force-name-id-format.tooltip' | translate}}</kc-tooltip>
             </div>
+            <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
+                <label class="col-md-2 control-label" for="samlAllowECPFlow">{{:: 'allow-ecp-flow' | translate}}</label>
+                <div class="col-sm-6">
+                    <input ng-model="samlAllowECPFlow" ng-click="switchChange()" name="samlAllowECPFlow" id="samlAllowECPFlow" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+                </div>
+                <kc-tooltip>{{:: 'allow-ecp-flow.tooltip' | translate}}</kc-tooltip>
+            </div>
             <div class="form-group" data-ng-show="protocol == 'saml'">
                 <label class="col-md-2 control-label" for="samlNameIdFormat">{{:: 'name-id-format' | translate}}</label>
                 <div class="col-sm-6">
                     <div>
                         <select class="form-control" id="samlNameIdFormat"

Vulnerable code: import java.util.Map;
import static org.junit.Assert.assertThat;
                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ONETIMEUSE_CONDITION;
Fixed code:     public boolean allowECPFlow() {
        return "true".equals(resolveAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW));
    }

    public void setAllowECPFlow(boolean val) {
        client.setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, Boolean.toString(val));
    }

    String SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";
        if (rep.getAllowEcpFlow() == null) {
            client.setAllowECPFlow(false);
        }

    public String getAllowEcpFlow() {
        if (getAttributes() == null) return null;
        return getAttributes().get(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW);
    }

import org.keycloak.protocol.saml.SamlClient;
import org.keycloak.services.managers.AuthenticationManager;
                    // Do not allow ECP login when client does not support it
                    if (!new SamlClient(client).allowECPFlow()) {
                        logger.errorf("Client %s is not allowed to execute ECP flow", client.getClientId());
                        throw new RuntimeException("Client is not allowed to use ECP profile.");
                    }

        // Saml ECP flow creates only TRANSIENT user sessions
        authSession.setClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString());
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import javax.ws.rs.core.Response;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;

import java.io.IOException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;

    @Test
    public void soapBindingIsNotPossibleForClientsWithSamlEcpFlowAttributeFalse() {
        // Disable ECP_FLOW_ENABLED switch
        getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
                .setAttribute(SamlConfigAttributes.SAML_ALLOW_ECP_FLOW, "false")
                .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
                .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
                .update());

        new SamlClientBuilder()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
                .basicAuthentication(bburkeUser)
                .build()
                .execute(response -> {
                    assertThat(response, statusCodeIsHC(Response.Status.INTERNAL_SERVER_ERROR));

                    try {
                        MessageFactory messageFactory = MessageFactory.newInstance();
                        SOAPMessage soapMessage = messageFactory.createMessage(null, response.getEntity().getContent());
                        String faultDetail = soapMessage.getSOAPBody().getFault().getDetail().getValue();
                        assertThat(faultDetail, is(equalTo("Client is not allowed to use ECP profile.")));
                    } catch (SOAPException | IOException e) {
                        throw new RuntimeException(e);
                    }
                });

    }

    @Test
    public void ecpFlowCreatesTransientSessions() {
        // Disable ECP_FLOW_ENABLED switch
        getCleanup().addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_ECP_SP)
                .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "false")
                .setAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, "false")
                .update());

        // Successfully login using ECP flow
        SAML2Object samlObject = new SamlClientBuilder()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_ECP_SP, SAML_ASSERTION_CONSUMER_URL_ECP_SP, SOAP)
                .basicAuthentication(bburkeUser)
                .build()
                .executeAndTransform(SOAP::extractResponse).getSamlObject();

        assertThat(samlObject, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        ResponseType loginResp1 = (ResponseType) samlObject;
        AuthnStatementType sessionId = (AuthnStatementType) loginResp1.getAssertions().get(0).getAssertion().getStatements().iterator().next();

        String userSessionId = sessionId.getSessionIndex().split("::")[0];

        // Test that the user session with the given ID does not exist
        testingClient.server().run(session -> {
            RealmModel realmByName = session.realms().getRealmByName(REALM_NAME);
            UserSessionModel userSession = session.sessions().getUserSession(realmByName, userSessionId);

            assertThat(userSession, nullValue());
        });


    }
                "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==",
                "saml.allow.ecp.flow": "true"
import org.keycloak.protocol.saml.SamlConfigAttributes;
        public static final String SAML_ALLOW_ECP_FLOW = SamlConfigAttributes.SAML_ALLOW_ECP_FLOW;
import static org.keycloak.testsuite.console.page.clients.settings.ClientSettingsForm.SAMLClientSettingsForm.SAML_ALLOW_ECP_FLOW;
        attributes.put(SAML_ALLOW_ECP_FLOW, "false");
allow-ecp-flow=Allow ECP Flow
allow-ecp-flow.tooltip=This client is allowed to use ECP flow for authenticating users.
    $scope.samlAllowECPFlow = false;
        if ($scope.client.attributes["saml.allow.ecp.flow"]) {
            if ($scope.client.attributes["saml.allow.ecp.flow"] == "true") {
                $scope.samlAllowECPFlow = true;
            } else {
                $scope.samlAllowECPFlow = false;
            }
        }
        }
        if ($scope.samlAllowECPFlow == true) {
            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "true";
        } else {
            $scope.clientEdit.attributes["saml.allow.ecp.flow"] = "false";

            <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
                <label class="col-md-2 control-label" for="samlAllowECPFlow">{{:: 'allow-ecp-flow' | translate}}</label>
                <div class="col-sm-6">
                    <input ng-model="samlAllowECPFlow" ng-click="switchChange()" name="samlAllowECPFlow" id="samlAllowECPFlow" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
                </div>
                <kc-tooltip>{{:: 'allow-ecp-flow.tooltip' | translate}}</kc-tooltip>
            </div>