package org.keycloak.testsuite.saml;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDMappingResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlProtocolUtils;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.testsuite.util.SamlUtils;
import org.keycloak.testsuite.util.saml.HandleArtifactStepBuilder;
import org.keycloak.testsuite.util.saml.SamlMessageReceiver;
import org.keycloak.testsuite.util.saml.SessionStateChecker;
import org.keycloak.testsuite.utils.io.IOUtil;
import org.w3c.dom.Document;

import jakarta.ws.rs.core.Response;
import javax.xml.transform.dom.DOMSource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.emptyOrNullString;
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.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.keycloak.testsuite.util.Matchers.bodyHC;
import static org.keycloak.testsuite.util.Matchers.isSamlLogoutRequest;
import static org.keycloak.testsuite.util.Matchers.isSamlResponse;
import static org.keycloak.testsuite.util.Matchers.isSamlStatusResponse;
import static org.keycloak.testsuite.util.Matchers.statusCodeIsHC;
import static org.keycloak.testsuite.util.SamlClient.Binding.ARTIFACT_RESPONSE;
import static org.keycloak.testsuite.util.SamlClient.Binding.POST;
import static org.keycloak.testsuite.util.SamlClient.Binding.REDIRECT;
import static org.keycloak.testsuite.util.SamlUtils.getSPInstallationDescriptor;

public class ArtifactBindingTest extends AbstractSamlTest {

    @Rule
    public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);

    /************************ LOGIN TESTS ************************/

    @Test
    public void testArtifactBindingTimesOutAfterCodeToTokenLifespan() throws Exception {
        
        getCleanup().addCleanup(
                new RealmAttributeUpdater(adminClient.realm(REALM_NAME))
                        .setAccessCodeLifespan(1)
                        .update()
        );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                    .setBeforeStepChecks(() -> setTimeOffset(1000)) // Move in time before resolving the artifact
                .build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), nullValue());
    }

    @Test
    public void testArtifactBindingWithResponseAndAssertionSignature() throws Exception {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST_ASSERTION_AND_RESPONSE_SIG, POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                    .build()
                .login()
                    .user(bburkeUser)
                    .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ASSERTION_AND_RESPONSE_SIG)
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                    .build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(samlResponse.getAssertions().get(0).getAssertion().getSignature(), not(nullValue()));

        SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-assertion-and-response-sig");
        SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator()); // Checks the signature of the response as well as the signature of the assertion
    }

    @Test
    public void testArtifactBindingWithEncryptedAssertion() throws Exception {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ENC,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST_ENC, POST)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .signWith(SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY, SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY)
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_ENC)
                .signWith(SAML_CLIENT_SALES_POST_ENC_PRIVATE_KEY, SAML_CLIENT_SALES_POST_ENC_PUBLIC_KEY)
                .build()
                .doNotFollowRedirects()
                .executeAndTransform(ARTIFACT_RESPONSE::extractResponse);

        assertThat(response.getSamlObject(), instanceOf(ResponseType.class));
        ResponseType loginResponse = (ResponseType) response.getSamlObject();
        assertThat(loginResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(loginResponse.getAssertions().get(0).getAssertion(), nullValue());
        assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), not(nullValue()));

        SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post-enc");
        AssertionUtil.decryptAssertion(loginResponse, deployment.getDecryptionKey());

        assertThat(loginResponse.getAssertions().get(0).getAssertion(), not(nullValue()));
        assertThat(loginResponse.getAssertions().get(0).getEncryptedAssertion(), nullValue());
        assertThat(loginResponse.getAssertions().get(0).getAssertion().getIssuer().getValue(), equalTo(getAuthServerRealmBase(REALM_NAME).toString()));
    }

    @Test
    public void testArtifactBindingLoginCheckArtifactWithPost() throws NoSuchAlgorithmException {
        String response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build().doNotFollowRedirects().executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));
        assertThat(response, containsString(GeneralConstants.SAML_ARTIFACT_KEY));

        Pattern artifactPattern = Pattern.compile("NAME=\"SAMLart\" VALUE=\"((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))");
        Matcher m = artifactPattern.matcher(response);
        assertThat(m.find(), is(true));

        String artifactB64 = m.group(1);
        assertThat(artifactB64,not(is(emptyOrNullString())));

        byte[] artifact = Base64.getDecoder().decode(artifactB64);
        assertThat(artifact.length, is(44));
        assertThat(artifact[0], is((byte)0));
        assertThat(artifact[1], is((byte)4));
        assertThat(artifact[2], is((byte)0));
        assertThat(artifact[3], is((byte)0));

        MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
        byte[] source = sha1Digester.digest(getAuthServerRealmBase(REALM_NAME).toString().getBytes(StandardCharsets.UTF_8));
        for (int i = 0; i < 20; i++) {
            assertThat(source[i], is(artifact[i+4]));
        }
    }

    @Test
    public void testArtifactBindingLoginFullExchangeWithPost() {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build().handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        assertThat(artifactResponse.getInResponseTo(), not(is(emptyOrNullString())));
        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }


    @Test
    public void testArtifactBindingLoginCorrectSignature() {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, SamlClient.Binding.POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
                        , SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                .build()
                .login().user(bburkeUser).build().handleArtifact(getAuthServerSamlEndpoint(REALM_NAME)
                        , SAML_CLIENT_ID_SALES_POST_SIG).signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
                        , SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY).build()
                .doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        assertThat(artifactResponse.getSignature(), not(nullValue()));

        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(samlResponse.getAssertions().get(0).getAssertion().getSignature(), nullValue());

    }

    @Test
    public void testArtifactBindingLoginIncorrectSignature() {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, SamlClient.Binding.POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY
                    , SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                .build()
                .login().user(bburkeUser).build()

                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME)
                        , SAML_CLIENT_ID_SALES_POST_SIG).signWith(SAML_CLIENT_SALES_POST_SIG_EXPIRED_PRIVATE_KEY,
                        SAML_CLIENT_SALES_POST_SIG_EXPIRED_PUBLIC_KEY)
                .build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), nullValue());
        assertThat(artifactResponse.getSignature(), not(nullValue()));
    }

    @Test
    public void testArtifactBindingLoginGetArtifactResponseTwice() {
        SamlClientBuilder clientBuilder = new SamlClientBuilder();
        HandleArtifactStepBuilder handleArtifactBuilder = new HandleArtifactStepBuilder(
                getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, clientBuilder);

        SAMLDocumentHolder response= clientBuilder.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.REDIRECT)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(handleArtifactBuilder).build()
                .processSamlResponse(ARTIFACT_RESPONSE)
                    .transformObject(ob -> {
                        assertThat(ob, isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
                        return null;
                    })
                .build()
                .handleArtifact(handleArtifactBuilder).replayPost(true).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), nullValue());
    }

    @Test
    public void testArtifactSuccessfulAfterFirstUnsuccessfulRequest() {
        SamlClientBuilder clientBuilder = new SamlClientBuilder();

        AtomicReference<String> artifact = new AtomicReference<>();

        SAMLDocumentHolder response = clientBuilder.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()

                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2) // Wrong issuer
                .storeArtifact(artifact)
                .build()
                .assertResponse(r -> assertThat(r, bodyHC(containsString(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()))))

                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                .useArtifact(artifact)
                .build()
                .executeAndTransform(ARTIFACT_RESPONSE::extractResponse);

        assertThat(response.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

    @Test
    public void testArtifactBindingLoginForceArtifactBinding() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

    @Test
    public void testArtifactBindingLoginSignedArtifactResponse() throws Exception {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);


        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), notNullValue());
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        assertThat(artifactResponse.getInResponseTo(), not(is(emptyOrNullString())));
        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));

        SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post");
        SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator());
    }

    @Test
    public void testArtifactBindingLoginFullExchangeWithRedirect() {
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.REDIRECT)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        assertThat(artifactResponse.getInResponseTo(), not(is(emptyOrNullString())));
        ResponseType samlResponse = (ResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

    @Test
    public void testArtifactResponseContainsCorrectInResponseTo(){
        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).setArtifactResolveId("TestId").build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse.getAny(), instanceOf(ResponseType.class));
        assertThat(artifactResponse.getInResponseTo(), is("TestId"));
    }

    /************************ LOGOUT TESTS ************************/

    @Test
    public void testArtifactBindingLogoutSingleClientCheckArtifact() throws NoSuchAlgorithmException {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlConfigAttributes.SAML_SERVER_SIGNATURE, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        String response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, SamlClient.Binding.POST)
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SamlClient.Binding.POST).build()
                .doNotFollowRedirects()
                .executeAndTransform(resp -> EntityUtils.toString(resp.getEntity()));

        assertThat(response, containsString(GeneralConstants.SAML_ARTIFACT_KEY));
        Pattern artifactPattern = Pattern.compile("NAME=\"SAMLart\" VALUE=\"((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))");
        Matcher m = artifactPattern.matcher(response);
        assertThat(true, is(m.find()));

        String artifactB64 = m.group(1);
        assertThat(artifactB64, not(is(emptyOrNullString())));

        byte[] artifact = Base64.getDecoder().decode(artifactB64);
        assertThat(artifact.length, is(44));
        assertThat(artifact[0], is((byte)0));
        assertThat(artifact[1], is((byte)4));
        assertThat(artifact[2], is((byte)0));
        assertThat(artifact[3], is((byte)0));

        MessageDigest sha1Digester = MessageDigest.getInstance("SHA-1");
        byte[] source = sha1Digester.digest(getAuthServerRealmBase(REALM_NAME).toString().getBytes(StandardCharsets.UTF_8));
        for (int i = 0; i < 20; i++) {
            assertThat(source[i], is(artifact[i+4]));
        }
    }

    @Test
    public void testArtifactBindingLogoutSingleClientPost() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
        assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
        StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

    @Test
    public void testArtifactBindingLogoutSingleClientRedirect() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
        assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
        StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

    @Test
    public void testArtifactBindingLogoutTwoClientsPostWithSig() throws Exception {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST_SIG)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST).build()
                .login().user(bburkeUser).build()
                .processSamlResponse(POST)
                    .transformObject(this::extractNameIdAndSessionIndexAndTerminate)
                .build()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG, SAML_ASSERTION_CONSUMER_URL_SALES_POST_SIG, POST)
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                .build()
                .login().sso(true).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG)
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                .build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, POST)
                    .nameId(nameIdRef::get)
                    .sessionIndex(sessionIndexRef::get)
                .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST_SIG)
                    .signWith(SAML_CLIENT_SALES_POST_SIG_PRIVATE_KEY, SAML_CLIENT_SALES_POST_SIG_PUBLIC_KEY)
                .build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), notNullValue());
        assertThat(artifactResponse.getAny(), instanceOf(LogoutRequestType.class));

        SamlDeployment deployment = SamlUtils.getSamlDeploymentForClient("sales-post");
        SamlProtocolUtils.verifyDocumentSignature(response.getSamlDocument(), deployment.getIDP().getSignatureValidationKeyLocator());
    }

    @Test
    public void testArtifactBindingLogoutTwoClientsRedirect() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            )
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST2)
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        SAMLDocumentHolder response = new SamlClientBuilder()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REDIRECT)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri()).build()
                .login().user(bburkeUser).build()
                .processSamlResponse(REDIRECT)
                    .transformObject(this::extractNameIdAndSessionIndexAndTerminate)
                .build()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                        SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT).setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
                .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()   // This is a formal step
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, REDIRECT)
                    .nameId(nameIdRef::get)
                    .sessionIndex(sessionIndexRef::get)
                .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
                .doNotFollowRedirects()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(LogoutRequestType.class));
    }

    @Test
    public void testArtifactBindingWithBackchannelLogout() {
        try (SamlMessageReceiver backchannelLogoutReceiver = new SamlMessageReceiver(8082);
             ClientAttributeUpdater cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                     .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                     .setFrontchannelLogout(false)
                     .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, backchannelLogoutReceiver.getUrl())
                     .update()) {

            new SamlClientBuilder()
                    .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()).build()
                    .login().user(bburkeUser).build()
                    .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()

                    .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, POST)
                    .build()
                    .followOneRedirect()
                    .processSamlResponse(POST)
                        .transformObject(this::extractNameIdAndSessionIndexAndTerminate)
                    .build()
                    .execute();

            // We need new SamlClient so that logout is not done using cookie -> frontchannel logout
            new SamlClientBuilder()
                    // Initiate logout as SAML_CLIENT_ID_SALES_POST2 and expect backchannel call to SAML_CLIENT_ID_SALES_POST
                    .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, POST)
                        .nameId(nameIdRef::get)
                        .sessionIndex(sessionIndexRef::get)
                    .build()
                    .executeAndTransform(r -> {
                        SAMLDocumentHolder saml2ObjectHolder = POST.extractResponse(r);
                        assertThat(saml2ObjectHolder.getSamlObject(), isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));

                        return null;
                    });



            // Check whether logoutReceiver contains correct LogoutRequest
            await()
                    .pollInterval(100, TimeUnit.MILLISECONDS)
                    .atMost(1, TimeUnit.MINUTES)
                    .until(backchannelLogoutReceiver::isMessageReceived);

            assertThat(backchannelLogoutReceiver.isMessageReceived(), is(true));
            SAMLDocumentHolder message = backchannelLogoutReceiver.getSamlDocumentHolder();

            assertThat(message.getSamlObject(), isSamlLogoutRequest(backchannelLogoutReceiver.getUrl()));
        } catch (Exception e) {
            throw new RuntimeException("Cannot run SamlMessageReceiver", e);
        }
    }

    @Test
    public void testArtifactResolveWithWrongIssuerFails() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
                    .update()
            );

        new SamlClientBuilder()
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()).build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2).build() // Wrong issuer
                .execute(r -> assertThat(r, bodyHC(containsString(JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get()))));
    }

     // Won't work with openshift, because openshift wouldn't see ArtifactResolutionService
    @Test
    public void testSessionStateDuringArtifactBindingLogoutWithOneClient() {
        ClientRepresentation salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
        final String clientId = salesRep.getId();

        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                        .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                        .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                        .setFrontchannelLogout(true)
                        .update()
            );

        AtomicReference<String> userSessionId = new AtomicReference<>();

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
                .build()
                .login().user(bburkeUser)
                .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .storeUserSessionId(userSessionId)
                        .expectedState(UserSessionModel.State.LOGGED_IN)
                        .expectedClientSession(clientId)
                        .consumeUserSession(userSessionModel -> assertThat(userSessionModel, notNullValue()))
                        .consumeClientSession(clientId, userSessionModel -> assertThat(userSessionModel, notNullValue())))
                    .build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .expectedUserSession(userSessionId)
                        .expectedState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED)
                        .expectedNumberOfClientSessions(1)
                        .expectedAction(clientId, CommonClientSessionModel.Action.LOGGING_OUT))
                    .setAfterStepChecks(new SessionStateChecker(testingClient.server())
                        .consumeUserSession(userSessionModel -> assertThat(userSessionModel, nullValue()))
                        .setUserSessionProvider(session -> userSessionId.get()))
                    .build()
                .doNotFollowRedirects().executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse.getAny(), not(instanceOf(ResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(ArtifactResponseType.class)));
        assertThat(artifactResponse.getAny(), not(instanceOf(NameIDMappingResponseType.class)));
        assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
        StatusResponseType samlResponse = (StatusResponseType)artifactResponse.getAny();
        assertThat(samlResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
    }

     // Won't work with openshift, because openshift wouldn't see ArtifactResolutionService
    @Test
    public void testSessionStateDuringArtifactBindingLogoutWithMoreFrontChannelClients() {
        getCleanup()
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            )
            .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST2)
                    .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                    .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url")
                    .setFrontchannelLogout(true)
                    .update()
            );

        ClientRepresentation salesRep = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST).get(0);
        final String salesRepId = salesRep.getId();

        ClientRepresentation salesRep2 = adminClient.realm(REALM_NAME).clients().findByClientId(SAML_CLIENT_ID_SALES_POST2).get(0);
        final String salesRep2Id = salesRep2.getId();

        final AtomicReference<String> userSessionId = new AtomicReference<>();

        SAMLDocumentHolder response = new SamlClientBuilder()
                // Login first sales_post2 and resolve artifact
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, SAML_ASSERTION_CONSUMER_URL_SALES_POST2, REDIRECT)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri()).build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .storeUserSessionId(userSessionId)
                        .expectedClientSession(salesRep2Id)
                        .expectedState(UserSessionModel.State.LOGGED_IN)
                        .expectedNumberOfClientSessions(1)
                        .consumeUserSession(userSessionModel -> assertThat(userSessionModel, notNullValue()))
                        .consumeClientSession(salesRep2Id, clientSession -> assertThat(clientSession, notNullValue())))
                    .verifyRedirect(true)
                .build()   // This is a formal step

                // Login second sales_post and resolved artifact, no login should be needed as user is already logged in
                .authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                        SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
                    .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
                .build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .expectedUserSession(userSessionId)
                        .expectedState(UserSessionModel.State.LOGGED_IN)
                        .expectedClientSession(salesRepId)
                        .expectedNumberOfClientSessions(2)
                        .expectedAction(salesRep2Id, null)
                        .expectedAction(salesRepId, null))
                    .verifyRedirect(true)
                .build()

                // Initiate logout from sales_post2
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2, REDIRECT)
                .build()

                // Since sales_post uses frontchannel logout, keycloak should send LogoutRequest to sales_post first
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .expectedUserSession(userSessionId)
                        .expectedState(UserSessionModel.State.LOGGING_OUT)
                        .expectedClientSession(salesRepId)
                        .expectedNumberOfClientSessions(2)
                        .expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGING_OUT)
                        .expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
                    .setAfterStepChecks(new SessionStateChecker(testingClient.server())
                        .setUserSessionProvider(session -> userSessionId.get())
                        .expectedState(UserSessionModel.State.LOGGING_OUT)
                        .expectedNumberOfClientSessions(2)
                        .expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGED_OUT)
                        .expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
                    .verifyRedirect(true)
                    .build()
                .doNotFollowRedirects()

                // Respond with LogoutResponse so that logout flow can continue with logging out client2
                .processSamlResponse(ARTIFACT_RESPONSE)
                    .transformDocument(doc -> {
                        // Send LogoutResponse
                        SAML2Object so = (SAML2Object) SAMLParser.getInstance().parse(new DOMSource(doc));
                        return new SAML2LogoutResponseBuilder()
                                .destination(getAuthServerSamlEndpoint(REALM_NAME).toString())
                                .issuer(SAML_CLIENT_ID_SALES_POST)
                                .logoutRequestID(((LogoutRequestType) so).getID())
                                .buildDocument();
                    })
                    .targetBinding(REDIRECT)
                    .targetAttributeSamlResponse()
                    .targetUri(getAuthServerSamlEndpoint(REALM_NAME))
                    .build()

                // Now Keycloak should finish logout process so it should respond with LogoutResponse
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST2)
                    .verifyRedirect(true)
                    .setBeforeStepChecks(new SessionStateChecker(testingClient.server())
                        .expectedUserSession(userSessionId)
                        .expectedClientSession(salesRep2Id)
                        .expectedState(UserSessionModel.State.LOGGED_OUT_UNCONFIRMED)
                        .expectedNumberOfClientSessions(2)
                        .expectedAction(salesRepId, CommonClientSessionModel.Action.LOGGED_OUT)
                        .expectedAction(salesRep2Id, CommonClientSessionModel.Action.LOGGING_OUT))
                    .setAfterStepChecks(new SessionStateChecker(testingClient.server())
                        .consumeUserSession(userSessionModel -> assertThat(userSessionModel, nullValue()))
                        .setUserSessionProvider(session -> userSessionId.get()))
                    .build()
                .executeAndTransform(this::getArtifactResponse);

        assertThat(response.getSamlObject(), instanceOf(ArtifactResponseType.class));
        ArtifactResponseType artifactResponse = (ArtifactResponseType)response.getSamlObject();
        assertThat(artifactResponse.getSignature(), nullValue());
        assertThat(artifactResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(artifactResponse.getAny(), instanceOf(StatusResponseType.class));
    }
    
    @Test
    public void testArtifactBindingIsNotUsedForLogoutWhenLogoutUrlNotSetRedirect() {
        getCleanup()
                .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                        .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                        .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE, "http://url")
                        .setFrontchannelLogout(true)
                        .update()
                );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, REDIRECT)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).verifyRedirect(true).build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, REDIRECT).build()
                .doNotFollowRedirects()
                .executeAndTransform(REDIRECT::extractResponse);

        assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
        StatusResponseType logoutResponse = (StatusResponseType)response.getSamlObject();
        assertThat(logoutResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(logoutResponse.getSignature(), nullValue());
        assertThat(logoutResponse, not(instanceOf(ResponseType.class)));
        assertThat(logoutResponse, not(instanceOf(ArtifactResponseType.class)));
        assertThat(logoutResponse, not(instanceOf(NameIDMappingResponseType.class)));
        assertThat(logoutResponse, instanceOf(StatusResponseType.class));
    }

    @Test
    public void testArtifactBindingIsNotUsedForLogoutWhenLogoutUrlNotSetPostTest() {
        getCleanup()
                .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                        .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                        .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE, "http://url")
                        .setFrontchannelLogout(true)
                        .update()
                );

        SAMLDocumentHolder response = new SamlClientBuilder().authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST,
                SAML_ASSERTION_CONSUMER_URL_SALES_POST, POST)
                .setProtocolBinding(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri())
                .build()
                .login().user(bburkeUser).build()
                .handleArtifact(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST).build()
                .logoutRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_SALES_POST, POST).build()
                .doNotFollowRedirects()
                .executeAndTransform(POST::extractResponse);

        assertThat(response.getSamlObject(), instanceOf(StatusResponseType.class));
        StatusResponseType logoutResponse = (StatusResponseType)response.getSamlObject();
        assertThat(logoutResponse, isSamlStatusResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
        assertThat(logoutResponse.getSignature(), nullValue());
        assertThat(logoutResponse, not(instanceOf(ResponseType.class)));
        assertThat(logoutResponse, not(instanceOf(ArtifactResponseType.class)));
        assertThat(logoutResponse, not(instanceOf(NameIDMappingResponseType.class)));
        assertThat(logoutResponse, instanceOf(StatusResponseType.class));
    }

    private SAMLDocumentHolder getArtifactResponse(CloseableHttpResponse response) throws IOException, ParsingException, ProcessingException {
        assertThat(response, statusCodeIsHC(Response.Status.OK));
        Document soapBody = extractSoapMessage(response);
        return SAML2Request.getSAML2ObjectFromDocument(soapBody);
    }

    private Document extractSoapMessage(CloseableHttpResponse response) throws IOException {
        ByteArrayInputStream bais = new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
        Document soapBody = Soap.extractSoapMessage(bais);
        response.close();
        return soapBody;
    }

    /************************ IMPORT CLIENT TESTS ************************/
    @Test
    public void testImportClientArtifactResolutionSingleServices() {
        Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-simple.xml"));
        ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap"));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
    }

    @Test
    public void testImportClientMultipleServices() {
        Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-multiple.xml"));
        ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap-1"));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), Matchers.startsWith("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
    }

    @Test
    public void testImportClientMultipleServicesWithDefault() {
        Document doc = IOUtil.loadXML(ArtifactBindingTest.class.getResourceAsStream("/saml/sp-metadata-artifact-multiple-default.xml"));
        ClientRepresentation clientRep = adminClient.realm(REALM_NAME).convertClientDescription(IOUtil.documentToString(doc));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE), is("https://test.keycloak.com/auth/login/epd/callback/soap-9"));
        assertThat(clientRep.getAttributes().get(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE), Matchers.startsWith("https://test.keycloak.com/auth/login/epd/callback/http-artifact"));
    }

    @Test
    public void testSPMetadataArtifactBindingNotUsedForLogout() throws ParsingException, URISyntaxException {

        getCleanup()
                .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                        .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                        .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
                        .setAdminUrl("http://admin.url.test")
                        .update()
                );
        SPSSODescriptorType spDescriptor = getSPInstallationDescriptor(adminClient.realm(REALM_NAME).clients(), SAML_CLIENT_ID_SALES_POST);
        assertThat(spDescriptor.getAssertionConsumerService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
        assertThat(spDescriptor.getAssertionConsumerService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
        
        assertThat(spDescriptor.getSingleLogoutService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri())));
        assertThat(spDescriptor.getSingleLogoutService().get(0).getLocation(), is(equalTo(new URI("http://admin.url.test"))));
    }

    @Test
    public void testSPMetadataArtifactBindingUsedForLogout() throws ParsingException, URISyntaxException {
        getCleanup()
                .addCleanup(ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
                        .setAttribute(SamlConfigAttributes.SAML_ARTIFACT_BINDING, "true")
                        .setAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
                        .setAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE, "http://url.artifact.test")
                        .setAdminUrl("http://admin.url.test")
                        .update()
                );

        SPSSODescriptorType spDescriptor = getSPInstallationDescriptor(adminClient.realm(REALM_NAME).clients(), SAML_CLIENT_ID_SALES_POST);
        assertThat(spDescriptor.getAssertionConsumerService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
        assertThat(spDescriptor.getAssertionConsumerService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));

        assertThat(spDescriptor.getSingleLogoutService().get(0).getBinding(), is(equalTo(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri())));
        assertThat(spDescriptor.getSingleLogoutService().get(0).getLocation(), is(equalTo(new URI("http://url.artifact.test"))));
    }

    @Test
    public void testArtifactBindingIdentifierChangedWhenClientIdChanged() throws IOException {
        ClientRepresentation clientRepresentation = adminClient.realm(REALM_NAME)
                .clients()
                .findByClientId(SAML_CLIENT_ID_SALES_POST)
                .get(0);

        String oldIdentifier = clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER);
        assertThat(oldIdentifier, notNullValue());

        final String newClientId = "new_client_id";

        try (ClientAttributeUpdater cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_SALES_POST)
            .setClientId(newClientId)
             .update()
        ) {
            clientRepresentation = adminClient.realm(REALM_NAME)
                    .clients()
                    .findByClientId(newClientId)
                    .get(0);

            String identifier = clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER);

            assertThat(identifier, not(equalTo(oldIdentifier)));
            assertThat(identifier, equalTo(ArtifactBindingUtils.computeArtifactBindingIdentifierString(newClientId)));
        }

        clientRepresentation = adminClient.realm(REALM_NAME)
                .clients()
                .findByClientId(SAML_CLIENT_ID_SALES_POST)
                .get(0);

        assertThat(clientRepresentation.getAttributes().get(SamlConfigAttributes.SAML_ARTIFACT_BINDING_IDENTIFIER), equalTo(oldIdentifier));
    }

}
