/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.keycloak.protocol.saml;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import jakarta.xml.soap.SOAPException;
import jakarta.xml.soap.SOAPMessage;

import org.keycloak.broker.saml.SAMLDataMarshaller;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ClientData;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.mappers.NameIdMapperHelper;
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
import org.keycloak.protocol.saml.mappers.SAMLNameIdMapper;
import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.SAML2LoginResponseBuilder;
import org.keycloak.saml.SAML2LogoutRequestBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAML2NameIDBuilder;
import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;

import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jboss.logging.Logger;
import org.w3c.dom.Document;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class SamlProtocol implements LoginProtocol {
    public static final String ATTRIBUTE_TRUE_VALUE = "true";
    public static final String ATTRIBUTE_FALSE_VALUE = "false";
    public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post";
    public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect";
    public static final String SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE = "saml_artifact_binding_url";
    public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
    public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE = "saml_single_logout_service_url_artifact";
    public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
    public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE = "saml_single_logout_service_url_soap";
    public static final String SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE = "saml_artifact_resolution_service_url";
    public static final String LOGIN_PROTOCOL = "saml";
    public static final String SAML_BINDING = "saml_binding";
    public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
    public static final String SAML_POST_BINDING = "post";
    public static final String SAML_ARTIFACT_BINDING = "artifact";
    public static final String SAML_SOAP_BINDING = "soap";
    public static final String SAML_REDIRECT_BINDING = "get";
    public static final String SAML_REQUEST_ID = "SAML_REQUEST_ID";
    public static final String SAML_REQUEST_ID_BROKER = "SAML_REQUEST_ID_BROKER";
    public static final String SAML_LOGOUT_BINDING = "saml.logout.binding";
    public static final String SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "saml.logout.addExtensionsElementWithKeyInfo";
    public static final String SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER = "SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER";
    public static final String SAML_LOGOUT_REQUEST_ID = "SAML_LOGOUT_REQUEST_ID";
    public static final String SAML_LOGOUT_RELAY_STATE = "SAML_LOGOUT_RELAY_STATE";
    public static final String SAML_LOGOUT_CANONICALIZATION = "SAML_LOGOUT_CANONICALIZATION";
    public static final String SAML_LOGOUT_BINDING_URI = "SAML_LOGOUT_BINDING_URI";
    public static final String SAML_LOGOUT_SIGNATURE_ALGORITHM = "saml.logout.signature.algorithm";
    public static final String SAML_NAME_ID = "SAML_NAME_ID";
    public static final String SAML_NAME_ID_FORMAT = "SAML_NAME_ID_FORMAT";
    public static final String SAML_DEFAULT_NAMEID_FORMAT = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
    public static final String SAML_PERSISTENT_NAME_ID_FOR = "saml.persistent.name.id.for";
    public static final String SAML_IDP_INITIATED_SSO_RELAY_STATE = "saml_idp_initiated_sso_relay_state";
    public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name";
    public static final String SAML_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN";
    public static final String SAML_FORCEAUTHN_REQUIREMENT = "true";
    public static final String SAML_LOGOUT_INITIATOR_CLIENT_ID = "SAML_LOGOUT_INITIATOR_CLIENT_ID";
    public static final String USER_SESSION_ID = "userSessionId";
    public static final String CLIENT_SESSION_ID = "clientSessionId";


    protected static final Logger logger = Logger.getLogger(SamlProtocol.class);

    protected KeycloakSession session;

    protected RealmModel realm;

    protected UriInfo uriInfo;

    protected HttpHeaders headers;

    protected EventBuilder event;

    protected ArtifactResolver artifactResolver;
    protected SingleUseObjectProvider singleUseStore;

    @Override
    public SamlProtocol setSession(KeycloakSession session) {
        this.session = session;
        return this;
    }

    @Override
    public SamlProtocol setRealm(RealmModel realm) {
        this.realm = realm;
        return this;
    }

    @Override
    public SamlProtocol setUriInfo(UriInfo uriInfo) {
        this.uriInfo = uriInfo;
        return this;
    }

    @Override
    public SamlProtocol setHttpHeaders(HttpHeaders headers) {
        this.headers = headers;
        return this;
    }

    @Override
    public SamlProtocol setEventBuilder(EventBuilder event) {
        this.event = event;
        return this;
    }

    private ArtifactResolver getArtifactResolver() {
        if (artifactResolver == null) {
            artifactResolver = session.getProvider(ArtifactResolver.class);
        }
        return artifactResolver;
    }

    private SingleUseObjectProvider getSingleUseStore() {
        if (singleUseStore == null) {
            singleUseStore = session.singleUseObjects();
        }
        return singleUseStore;
    }

    @Override
    public Response sendError(AuthenticationSessionModel authSession, Error error, String errorMessage) {
        try {
            ClientModel client = authSession.getClient();

            if ("true".equals(authSession.getClientNote(SAML_IDP_INITIATED_LOGIN))) {
                if (error == Error.CANCELLED_BY_USER) {
                    UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
                    Map<String, String> params = new HashMap<>();
                    params.put("realm", realm.getName());
                    params.put("protocol", LOGIN_PROTOCOL);
                    params.put("client", client.getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
                    URI redirect = builder.buildFromMap(params);
                    return Response.status(302).location(redirect).build();
                } else {
                    return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, errorMessage != null ? errorMessage : translateErrorToIdpInitiatedErrorMessage(error));
                }
            } else {
                return samlErrorMessage(
                        authSession, new SamlClient(client), isPostBinding(authSession),
                        authSession.getRedirectUri(), translateErrorToSAMLStatus(error), authSession.getClientNote(GeneralConstants.RELAY_STATE)
                );
            }
        } finally {
            // Remove authenticationSession of current browser tab
            new AuthenticationSessionManager(session).removeTabIdInAuthenticationSession(realm, authSession);
        }
    }

    @Override
    public ClientData getClientData(AuthenticationSessionModel authSession) {
        String responseMode = isPostBinding(authSession) ? SamlProtocol.SAML_POST_BINDING : SamlProtocol.SAML_REDIRECT_BINDING;
        return new ClientData(authSession.getRedirectUri(),
                null,
                responseMode,
                authSession.getClientNote(GeneralConstants.RELAY_STATE));
    }

    @Override
    public Response sendError(ClientModel client, ClientData clientData, Error error) {
        logger.tracef("Calling sendError with clientData when authenticating with client '%s' in realm '%s'. Error: %s", client.getClientId(), realm.getName(), error);

        SamlClient samlClient = new SamlClient(client);
        boolean postBinding = samlClient.forcePostBinding() || SamlProtocol.SAML_POST_BINDING.equals(clientData.getResponseMode());
        event.detail(Details.REDIRECT_URI, clientData.getRedirectUri());
        String validRedirectUri = RedirectUtils.verifyRedirectUri(session, clientData.getRedirectUri(), client);
        if (validRedirectUri == null) {
            event.error(Errors.INVALID_REDIRECT_URI);
            throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
        }

        return samlErrorMessage(
                null, samlClient, postBinding,
                validRedirectUri, translateErrorToSAMLStatus(error), clientData.getState()
        );
    }

    private Response samlErrorMessage(
            AuthenticationSessionModel authSession, SamlClient samlClient, boolean isPostBinding,
            String destination, SAMLError samlError, String relayState) {
        JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(relayState);
        SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(destination).issuer(getResponseIssuer(realm))
                .status(samlError.error().get())
                .statusMessage(samlError.errorDescription());
        KeyManager keyManager = session.keys();
        if (samlClient.requiresRealmSignature()) {
            KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
            String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
            String canonicalization = samlClient.getCanonicalizationMethod();
            if (canonicalization != null) {
                binding.canonicalizationMethod(canonicalization);
            }
            binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
        }

        try {
            // There is no support for encrypting status messages in SAML.
            // Only assertions, attributes, base ID and name ID can be encrypted
            // See Chapter 6 of saml-core-2.0-os.pdf
            Document document = builder.buildDocument();
            return buildErrorResponse(isPostBinding, destination, binding, document);
        } catch (Exception e) {
            return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
        }
    }

    protected Response buildErrorResponse(boolean isPostBinding, String destination, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException {
        if (isPostBinding) {
            return binding.postBinding(document).response(destination);
        } else {
            return binding.redirectBinding(document).response(destination);
        }
    }

    private SAMLError translateErrorToSAMLStatus(Error error) {
        switch (error) {
        case CANCELLED_BY_USER:
        case CANCELLED_AIA:
        case CONSENT_DENIED:
            return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
        case PASSIVE_INTERACTION_REQUIRED:
        case PASSIVE_LOGIN_REQUIRED:
            return new SAMLError(JBossSAMLURIConstants.STATUS_NO_PASSIVE, null);
        case ALREADY_LOGGED_IN:
            return new SAMLError(JBossSAMLURIConstants.STATUS_AUTHNFAILED, Constants.AUTHENTICATION_EXPIRED_MESSAGE);
        default:
            logger.warn("Untranslated protocol Error: " + error.name() + " so we return default SAML error");
            return new SAMLError(JBossSAMLURIConstants.STATUS_REQUEST_DENIED, null);
        }
    }

    private String translateErrorToIdpInitiatedErrorMessage(Error error) {
        switch (error) {
        case CONSENT_DENIED:
            return Messages.CONSENT_DENIED;
        case PASSIVE_INTERACTION_REQUIRED:
        case PASSIVE_LOGIN_REQUIRED:
            return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
        default:
            logger.warn("Untranslated protocol Error: " + error.name() + " so we return default error message");
            return Messages.UNEXPECTED_ERROR_HANDLING_REQUEST;
        }
    }

    protected String getResponseIssuer(RealmModel realm) {
        return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
    }

    protected boolean isPostBinding(AuthenticationSessionModel authSession) {
        ClientModel client = authSession.getClient();
        SamlClient samlClient = new SamlClient(client);
        return SamlProtocol.SAML_POST_BINDING.equals(authSession.getClientNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
    }

    protected boolean isPostBinding(AuthenticatedClientSessionModel clientSession) {
        ClientModel client = clientSession.getClient();
        SamlClient samlClient = new SamlClient(client);
        return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
    }

    public static boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
        String note = session.getNote(SamlProtocol.SAML_LOGOUT_BINDING);
        return SamlProtocol.SAML_POST_BINDING.equals(note);
    }

    public static String getLogoutBindingTypeForClientSession(AuthenticatedClientSessionModel clientSession) {
        ClientModel client = clientSession.getClient();
        SamlClient samlClient = new SamlClient(client);
        String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
        String logoutRedirectUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
        String logoutArtifactUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);

        boolean postUrlSet = !(logoutPostUrl == null || logoutPostUrl.trim().isEmpty());
        boolean redirectUrlSet = !(logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
        boolean artifactUrlSet = !(logoutArtifactUrl == null || logoutArtifactUrl.trim().isEmpty());

        // client configured to force post binding and postUrl for logout is set
        if (samlClient.forcePostBinding() && postUrlSet) {
            return SAML_POST_BINDING;
        }

        // client configured to force artifact binding and artifactUrl for logout is set
        if (samlClient.forceArtifactBinding() && artifactUrlSet) {
            return SAML_ARTIFACT_BINDING;
        }

        final String bindingType = clientSession.getNote(SAML_BINDING);

        // if the login binding was artifact and url set, return artifact
        if (SAML_ARTIFACT_BINDING.equals(bindingType) && artifactUrlSet) {
            return SAML_ARTIFACT_BINDING;
        }

        // if the login binding was POST and url set, return POST
        if (SAML_POST_BINDING.equals(bindingType) && postUrlSet) {
            return SAML_POST_BINDING;
        }

        // fall back to post binding if no redirect URL
        if (!redirectUrlSet && (postUrlSet || samlClient.forcePostBinding())) {
            return SAML_POST_BINDING;
        }

        // Default to Redirect
        return SAML_REDIRECT_BINDING;
    }

    protected String getNameIdFormat(SamlClient samlClient, AuthenticationSessionModel authSession) {
        String nameIdFormat = authSession.getClientNote(GeneralConstants.NAMEID_FORMAT);

        boolean forceFormat = samlClient.forceNameIDFormat();
        String configuredNameIdFormat = samlClient.getNameIDFormat();
        if ((nameIdFormat == null || forceFormat) && configuredNameIdFormat != null) {
            nameIdFormat = configuredNameIdFormat;
        }
        if (nameIdFormat == null)
            return SAML_DEFAULT_NAMEID_FORMAT;
        return nameIdFormat;
    }

    protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) {
        if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) {
            final String email = userSession.getUser().getEmail();
            if (email == null) {
                logger.debugf("E-mail of the user %s has to be set for %s NameIDFormat", userSession.getUser().getUsername(), JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get());
            }
            return email;
        } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) {
            // "G-" stands for "generated" Add this for the slight possibility of collisions.
            return "G-" + UUID.randomUUID().toString();
        } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())) {
            return getPersistentNameId(clientSession, userSession);
        } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
            // TODO: Support for persistent NameID (pseudo-random identifier persisted in user object)
            return userSession.getUser().getUsername();
        } else {
            return userSession.getUser().getUsername();
        }
    }

    /**
     * Attempts to retrieve the persistent type NameId as follows:
     *
     * <ol>
     * <li>saml.persistent.name.id.for.$clientId user attribute</li>
     * <li>saml.persistent.name.id.for.* user attribute</li>
     * <li>G-$randomUuid</li>
     * </ol>
     * <p>
     * If a randomUuid is generated, an attribute for the given saml.persistent.name.id.for.$clientId will be generated,
     * otherwise no state change will occur with respect to the user's attributes.
     *
     * @return the user's persistent NameId
     */
    protected String getPersistentNameId(final CommonClientSessionModel clientSession, final UserSessionModel userSession) {
        // attempt to retrieve the UserID for the client-specific attribute
        final UserModel user = userSession.getUser();
        final String clientNameId = String.format("%s.%s", SAML_PERSISTENT_NAME_ID_FOR,
                clientSession.getClient().getClientId());
        String samlPersistentNameId = user.getFirstAttribute(clientNameId);
        if (samlPersistentNameId != null) {
            return samlPersistentNameId;
        }

        // check for a wildcard attribute
        final String wildcardNameId = String.format("%s.*", SAML_PERSISTENT_NAME_ID_FOR);
        samlPersistentNameId = user.getFirstAttribute(wildcardNameId);
        if (samlPersistentNameId != null) {
            return samlPersistentNameId;
        }

        // default to generated.  "G-" stands for "generated"
        samlPersistentNameId = "G-" + UUID.randomUUID().toString();
        user.setSingleAttribute(clientNameId, samlPersistentNameId);
        return samlPersistentNameId;
    }

    @Override
    public Response authenticated(AuthenticationSessionModel authSession, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
        AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
        ClientModel client = clientSession.getClient();
        SamlClient samlClient = new SamlClient(client);
        String requestID = authSession.getClientNote(SAML_REQUEST_ID);
        String relayState = authSession.getClientNote(GeneralConstants.RELAY_STATE);
        String redirectUri = authSession.getRedirectUri();
        String responseIssuer = getResponseIssuer(realm);
        String nameIdFormat = getNameIdFormat(samlClient, authSession);

        int assertionLifespan = samlClient.getAssertionLifespan();
        SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder();
        builder.requestID(requestID)
                .destination(redirectUri)
                .issuer(responseIssuer)
                .assertionExpiration(assertionLifespan <= 0? realm.getAccessCodeLifespan() : assertionLifespan)
                .subjectExpiration(assertionLifespan <= 0? realm.getAccessTokenLifespan() : assertionLifespan)
                .sessionExpiration(realm.getSsoSessionMaxLifespan())
                .requestIssuer(clientSession.getClient().getClientId())
                .authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());

        String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession);
        builder.sessionIndex(sessionIndex);

        if (!samlClient.includeAuthnStatement()) {
            builder.disableAuthnStatement(true);
        }

        builder.includeOneTimeUseCondition(samlClient.includeOneTimeUseCondition());

        List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
        List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
        AtomicReference<ProtocolMapperProcessor<SAMLRoleListMapper>> roleListMapper = new AtomicReference<>(null);
        List<ProtocolMapperProcessor<SAMLNameIdMapper>> samlNameIdMappers = new LinkedList<>();

        ProtocolMapperUtils.getSortedProtocolMappers(session, clientSessionCtx)
                .forEach(entry -> {
                    ProtocolMapperModel mapping = entry.getKey();
                    ProtocolMapper mapper = entry.getValue();

                    if (mapper instanceof SAMLAttributeStatementMapper) {
                        attributeStatementMappers.add(new ProtocolMapperProcessor<>((SAMLAttributeStatementMapper) mapper, mapping));
                    }
                    if (mapper instanceof SAMLLoginResponseMapper) {
                        loginResponseMappers.add(new ProtocolMapperProcessor<>((SAMLLoginResponseMapper) mapper, mapping));
                    }
                    if (mapper instanceof SAMLRoleListMapper) {
                        roleListMapper.set(new ProtocolMapperProcessor<>((SAMLRoleListMapper) mapper, mapping));
                    }
                    if (mapper instanceof SAMLNameIdMapper) {
                        samlNameIdMappers.add(new ProtocolMapperProcessor<>((SAMLNameIdMapper) mapper, mapping));
                    }
                });

        Document samlDocument = null;
        ResponseType samlModel = null;
        KeyManager keyManager = session.keys();
        KeyWrapper keyPair = keyManager.getActiveKey(realm, KeyUse.SIG, Algorithm.RS256);
        boolean postBinding = isPostBinding(authSession);
        String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keyPair.getKid(), keyPair.getCertificate());
        String nameId = getSAMLNameId(samlNameIdMappers, nameIdFormat, session, userSession, clientSession);

        if (nameId == null) {
            return samlErrorMessage(null, samlClient, isPostBinding(authSession), redirectUri,
                    new SAMLError(JBossSAMLURIConstants.STATUS_INVALID_NAMEIDPOLICY, null), relayState);
        }

        builder.nameIdentifier(nameIdFormat, nameId);

        // save NAME_ID and format in clientSession as they may be persistent or
        // transient or email and not username
        // we'll need to send this back on a logout
        clientSession.setNote(SAML_NAME_ID, nameId);
        clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat);

        try {
            if ((!postBinding) && samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
                builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
            }

            samlModel = builder.buildModel();
            final AttributeStatementType attributeStatement = populateAttributeStatements(attributeStatementMappers, session, userSession, clientSession);
            populateRoles(roleListMapper.get(), session, userSession, clientSessionCtx, attributeStatement);

            // SAML Spec 2.7.3 AttributeStatement must contain one or more Attribute or EncryptedAttribute
            if (attributeStatement.getAttributes().size() > 0) {
                AssertionType assertion = samlModel.getAssertions().get(0).getAssertion();
                assertion.addStatement(attributeStatement);
            }

            samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSessionCtx);
        } catch (Exception e) {
            logger.error("failed", e);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
        }

        JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);
        bindingBuilder.relayState(relayState);

        if ("true".equals(clientSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
            try {
                return buildArtifactAuthenticatedResponse(clientSession, redirectUri, samlModel, bindingBuilder);
            } catch (Exception e) {
                logger.error("failed", e);
                return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
            }
        }

        if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
            String canonicalization = samlClient.getCanonicalizationMethod();
            if (canonicalization != null) {
                bindingBuilder.canonicalizationMethod(canonicalization);
            }
            bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, (PrivateKey) keyPair.getPrivateKey(), (PublicKey) keyPair.getPublicKey(), keyPair.getCertificate());

            if (samlClient.requiresRealmSignature()) bindingBuilder.signDocument();
            if (samlClient.requiresAssertionSignature()) bindingBuilder.signAssertions();
        }

        if (samlClient.requiresEncryption()) {
            try {
                SamlProtocolUtils.setupEncryption(session, samlClient, bindingBuilder);
            } catch (Exception e) {
                logger.error("failed", e);
                return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
            }
        }
        try {
            samlDocument = builder.buildDocument(samlModel);
            return buildAuthenticatedResponse(clientSession, redirectUri, samlDocument, bindingBuilder);
        } catch (Exception e) {
            logger.error("failed", e);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_TO_PROCESS_RESPONSE);
        }
    }

    protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException {
        if (isPostBinding(clientSession)) {
            return bindingBuilder.postBinding(samlDocument).response(redirectUri);
        } else {
            return bindingBuilder.redirectBinding(samlDocument).response(redirectUri);
        }
    }

    public static class ProtocolMapperProcessor<T> {
        public final T mapper;
        public final ProtocolMapperModel model;

        public ProtocolMapperProcessor(T mapper, ProtocolMapperModel model) {
            this.mapper = mapper;
            this.model = model;
        }
    }

    public AttributeStatementType populateAttributeStatements(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers, KeycloakSession session, UserSessionModel userSession,
                                                              AuthenticatedClientSessionModel clientSession) {
        AttributeStatementType attributeStatement = new AttributeStatementType();
        for (ProtocolMapperProcessor<SAMLAttributeStatementMapper> processor : attributeStatementMappers) {
            processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
        }

        return attributeStatement;
    }

    public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers, ResponseType response,
            KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
        for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
            response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSessionCtx);
        }

        for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext(); ) {
            response = (ResponseType) it.next().beforeSendingResponse(response, clientSessionCtx.getClientSession());
        }

        return response;
    }

    public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper, KeycloakSession session, UserSessionModel userSession,
                              ClientSessionContext clientSessionCtx, final AttributeStatementType existingAttributeStatement) {
        if (roleListMapper == null)
            return;
        roleListMapper.mapper.mapRoles(existingAttributeStatement, roleListMapper.model, session, userSession, clientSessionCtx);
    }

    protected String getSAMLNameId(
            List<ProtocolMapperProcessor<SAMLNameIdMapper>> samlNameIdMappers, String nameIdFormat, KeycloakSession session,
                                    UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
        for (ProtocolMapperProcessor<SAMLNameIdMapper> nameIdMap : samlNameIdMappers) {
            if(nameIdFormat.equals(nameIdMap.model.getConfig().get(NameIdMapperHelper.MAPPER_NAMEID_FORMAT))) {
                return nameIdMap.mapper.mapperNameId(nameIdFormat, nameIdMap.model, session, userSession, clientSession);
            }
        }
        return getNameId(nameIdFormat, clientSession, userSession);
    }

    public static String getLogoutServiceUrl(KeycloakSession session, ClientModel client, String bindingType, boolean backChannelLogout) {
        String logoutServiceUrl = null;

        if (SAML_SOAP_BINDING.equals(bindingType)) {
            // standard backchannel logout; cannot do front channel with SOAP binding
            // we do not allow this URL to be set through the management URL (it is a purely backend-oriented URL)
            logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_SOAP_ATTRIBUTE);
            return logoutServiceUrl == null || logoutServiceUrl.trim().equals("") ? null : logoutServiceUrl;
        } else if (!backChannelLogout && (SAML_ARTIFACT_BINDING.equals(bindingType) || useArtifactForLogout(client))) {
            // backchannel logout doesn't support sending artifacts
            logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE);
        } else if (SAML_POST_BINDING.equals(bindingType)) {
            logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
        } else {
            logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
        }

        if (logoutServiceUrl == null)
            logoutServiceUrl = client.getManagementUrl();
        if (logoutServiceUrl == null || logoutServiceUrl.trim().equals(""))
            return null;
        return ResourceAdminManager.resolveUri(session, client.getRootUrl(), logoutServiceUrl);
    }


    public static boolean useArtifactForLogout(ClientModel client) {
        return new SamlClient(client).forceArtifactBinding()
                && client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_ARTIFACT_ATTRIBUTE) != null;
    }

    @Override
    public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
        ClientModel client = clientSession.getClient();
        SamlClient samlClient = new SamlClient(client);
        try {
            String logoutBindingType = getLogoutBindingTypeForClientSession(clientSession);
            String bindingUri = getLogoutServiceUrl(session, client, logoutBindingType, false);
            if (bindingUri == null) {
                logger.warnf("Failed to logout client %s, skipping this client.  Please configure the logout service url in the admin console for your client applications.", client.getClientId());
                return null;
            }

            NodeGenerator[] extensions = new NodeGenerator[]{};
            if (!SAML_POST_BINDING.equals(logoutBindingType)) {
                if (samlClient.requiresRealmSignature() && samlClient.addExtensionsElementWithKeyInfo()) {
                    KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
                    String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
                    extensions = new NodeGenerator[]{new KeycloakKeySamlExtensionGenerator(keyName)};
                }
            }
            LogoutRequestType logoutRequest = createLogoutRequest(bindingUri, clientSession, client, extensions);
            JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, SAML_ARTIFACT_BINDING.equals(logoutBindingType));

            //If this session uses artifact binding, send an artifact instead of the LogoutRequest
            if (SAML_ARTIFACT_BINDING.equals(logoutBindingType)) {
                clientSession.setAction(CommonClientSessionModel.Action.LOGGING_OUT.name());
                return buildArtifactAuthenticatedResponse(clientSession, bindingUri, logoutRequest, binding);
            }

            Document samlDocument = SAML2Request.convert(logoutRequest);
            if (SAML_POST_BINDING.equals(logoutBindingType)) {
                // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
                return binding.postBinding(samlDocument).request(bindingUri);
            } else {
                logger.debug("frontchannel redirect binding");
                return binding.redirectBinding(samlDocument).request(bindingUri);
            }
        } catch (ConfigurationException | ProcessingException | IOException | ParsingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Response finishBrowserLogout(UserSessionModel userSession, AuthenticationSessionModel logoutSession) {
        logger.debug("finishLogout");
        String logoutBindingUri = userSession.getNote(SAML_LOGOUT_BINDING_URI);
        if (logoutBindingUri == null) {
            logger.error("Can't finish SAML logout as there is no logout binding set.  Please configure the logout service url in the admin console for your client applications.");
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);

        }
        String logoutRelayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE);
        SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
        builder.logoutRequestID(userSession.getNote(SAML_LOGOUT_REQUEST_ID));
        builder.destination(logoutBindingUri);
        builder.issuer(getResponseIssuer(realm));
        JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session);
        binding.relayState(logoutRelayState);
        String signingAlgorithm = userSession.getNote(SAML_LOGOUT_SIGNATURE_ALGORITHM);
        boolean postBinding = isLogoutPostBindingForInitiator(userSession);
        if (signingAlgorithm != null) {
            SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(signingAlgorithm);
            String canonicalization = userSession.getNote(SAML_LOGOUT_CANONICALIZATION);
            if (canonicalization != null) {
                binding.canonicalizationMethod(canonicalization);
            }
            KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
            XmlKeyInfoKeyNameTransformer transformer = XmlKeyInfoKeyNameTransformer.from(
                    userSession.getNote(SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER),
                    SamlClient.DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER);
            String keyName = transformer.getKeyName(keys.getKid(), keys.getCertificate());
            binding.signatureAlgorithm(algorithm).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
            boolean addExtension = (!postBinding) && Objects.equals("true", userSession.getNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO));
            if (addExtension) {    // Only include extension if REDIRECT binding and signing whole SAML protocol message
                builder.addExtension(new KeycloakKeySamlExtensionGenerator(keyName));
            }
        }
        Response response;
        try {
            response = buildLogoutResponse(userSession, logoutBindingUri, builder, binding);
        } catch (ConfigurationException | ProcessingException | IOException e) {
            throw new RuntimeException(e);
        }
        if (logoutBindingUri != null) {
            event.detail(Details.REDIRECT_URI, logoutBindingUri);
        }
        event.event(EventType.LOGOUT)
                .detail(Details.AUTH_METHOD, userSession.getAuthMethod())
                .client(session.getContext().getClient())
                .user(userSession.getUser())
                .session(userSession)
                .detail(Details.USERNAME, userSession.getLoginUsername())
                .detail(Details.RESPONSE_MODE, postBinding ? SamlProtocol.SAML_POST_BINDING : SamlProtocol.SAML_REDIRECT_BINDING)
                .detail(SamlProtocol.SAML_LOGOUT_REQUEST_ID, userSession.getNote(SAML_LOGOUT_REQUEST_ID))
                .success();
        return response;
    }

    protected Response buildLogoutResponse(UserSessionModel userSession, String logoutBindingUri, SAML2LogoutResponseBuilder builder, JaxrsSAML2BindingBuilder binding) throws ConfigurationException, ProcessingException, IOException {

        //if artifact binding is used, send an artifact instead of the LogoutResponse
        if ("true".equals(userSession.getNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get()))) {
            return buildLogoutArtifactResponse(userSession, logoutBindingUri, builder.buildModel(), binding);
        }

        Document samlDocument = builder.buildDocument();

        if (isLogoutPostBindingForInitiator(userSession)) {
            return binding.postBinding(samlDocument).response(logoutBindingUri);
        } else {
            return binding.redirectBinding(samlDocument).response(logoutBindingUri);
        }
    }

    @Override
    public Response backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
        ClientModel client = clientSession.getClient();
        SamlClient samlClient = new SamlClient(client);

        // real backchannel logout if SOAP binding is supported (#9548)
        String soapLogoutUrl = getLogoutServiceUrl(session, client, SAML_SOAP_BINDING, true);
        if (soapLogoutUrl != null) {
            try {
                LogoutRequestType logoutRequest = createLogoutRequest(soapLogoutUrl, clientSession, client);
                Document samlLogoutRequest = createBindingBuilder(samlClient, false).soapBinding(SAML2Request.convert(logoutRequest)).getDocument();
                SOAPMessage soapResponse = Soap.createMessage()
                        .addMimeHeader("SOAPAction", "http://www.oasis-open.org/committees/security") // MAY in SOAP binding spec
                        .addToBody(samlLogoutRequest)
                        .call(soapLogoutUrl, session);
                Document logoutResponse = Soap.extractSoapMessage(soapResponse);
                SAMLDocumentHolder samlDocResponse = SAML2Response.getSAML2ObjectFromDocument(logoutResponse);
                if (!validateLogoutResponse(logoutRequest, samlDocResponse, client)) {
                    return Response.serverError().build();
                }
                return Response.ok().build();
            } catch (SOAPException e) {
                logger.warnf(e, "Logout failed for client %s", client.getClientId());
                return Response.serverError().build();
            } catch (Exception e) {
                logger.warn("failed to execute saml soap logout", e);
                return Response.serverError().build();
            }
        }
        logger.warnf("Can't do SOAP backchannel logout. No SingleLogoutService SOAP Binding registered for client %s; fallback on legacy backchannel logout",
                client.getClientId());

        // legacy backchannel logout implementation (send POST / Redirect binding directly to the logout endpoint without going through the browser)
        String logoutUrl = getLogoutServiceUrl(session, client, SAML_POST_BINDING, true);
        if (logoutUrl == null) {
            logger.warnf("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: %s",
                    client.getClientId());
            return Response.serverError().build();
        }

        String logoutRequestString = null;
        try {
            LogoutRequestType logoutRequest = createLogoutRequest(logoutUrl, clientSession, client);
            JaxrsSAML2BindingBuilder binding = createBindingBuilder(samlClient, false);
            // This is POST binding, hence KeyID is included in dsig:KeyInfo/dsig:KeyName, no need to add <samlp:Extensions> element
            logoutRequestString = binding.postBinding(SAML2Request.convert(logoutRequest)).encoded();
        } catch (Exception e) {
            logger.warn("failed to send saml logout", e);
            return Response.serverError().build();
        }

        CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
        for (int i = 0; i < 2; i++) { // follow redirects once
            try {
                List<NameValuePair> formparams = new ArrayList<NameValuePair>();
                formparams.add(new BasicNameValuePair(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString));
                formparams.add(new BasicNameValuePair("BACK_CHANNEL_LOGOUT", "BACK_CHANNEL_LOGOUT")); // for Picketlink
                // todo remove
                // this
                UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, StandardCharsets.UTF_8);
                HttpPost post = new HttpPost(logoutUrl);
                post.setEntity(form);
                try (CloseableHttpResponse response = httpClient.execute(post)) {
                    try {
                        int status = response.getStatusLine().getStatusCode();
                        if (status == 302 && !logoutUrl.endsWith("/")) {
                            String redirect = response.getFirstHeader(HttpHeaders.LOCATION).getValue();
                            String withSlash = logoutUrl + "/";
                            if (withSlash.equals(redirect)) {
                                logoutUrl = withSlash;
                                continue;
                            }
                        }
                    } finally {
                        EntityUtils.consumeQuietly(response.getEntity());
                    }
                }
            } catch (IOException e) {
                logger.warn("failed to send saml logout", e);
                return Response.serverError().build();
            }
            break;
        }
        return Response.ok().build();
    }

    /**
     * Validate the logout response received by the client through the backchannel
     */
    private boolean validateLogoutResponse(LogoutRequestType logoutRequest, SAMLDocumentHolder holder, ClientModel client) {
        if (!(holder.getSamlObject() instanceof StatusResponseType)) {
            logger.warn("Logout response format is not valid");
            return false;
        }
        if (new SamlClient(client).requiresClientSignature()) {
            try {
                SamlProtocolUtils.verifyDocumentSignature(session, client, holder.getSamlDocument());
            } catch (VerificationException ex) {
                logger.warnf("Logout response from client %s contains invalid signature", client.getClientId());
                return false;
            }
        }
        StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
        String issuer = statusResponse.getIssuer().getValue();
        if (!client.getClientId().equals(issuer)) {
            logger.warn("Logout response contains wrong 'issuer' value");
            return false;
        }
        // check inResponseTo field of response
        if (!logoutRequest.getID().equals(statusResponse.getInResponseTo())) {
            logger.warn("Logout response contains wrong 'inResponseTo' value");
            return false;
        }
        return true;
    }

    protected LogoutRequestType createLogoutRequest(String logoutUrl, AuthenticatedClientSessionModel clientSession, ClientModel client, NodeGenerator... extensions) throws ConfigurationException {
        // build userPrincipal with subject used at login
        SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm))
                .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl);

        String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession);
        logoutBuilder.sessionIndex(sessionIndex);

        for (NodeGenerator extension : extensions) {
            logoutBuilder.addExtension(extension);
        }
        LogoutRequestType logoutRequest = logoutBuilder.createLogoutRequest();
        for (Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext(); ) {
            logoutRequest = it.next().beforeSendingLogoutRequest(logoutRequest, clientSession.getUserSession(), clientSession);
        }

        return logoutRequest;
    }

    @Override
    public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) {
        String requireReauthentication = authSession.getAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN);
        return Objects.equals(SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT, requireReauthentication);
    }

    private JaxrsSAML2BindingBuilder createBindingBuilder(SamlClient samlClient, boolean skipRealmSignature) {
        JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session);
        if (!skipRealmSignature && samlClient.requiresRealmSignature()) {
            KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
            String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
            binding.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
        }
        return binding;
    }

    @Override
    public void close() {

    }

    /**
     * This method, instead of sending the actual response with the token sends
     * the artifact message via post or redirect.
     *
     * @param clientSession  the current authenticated client session
     * @param redirectUri    the redirect uri to the client
     * @param samlDocument   a Document containing the saml Response
     * @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
     * @return A response (POSTed form or redirect) with a newly generated artifact
     * @throws ConfigurationException
     * @throws ProcessingException
     * @throws IOException
     */
    protected Response buildArtifactAuthenticatedResponse(AuthenticatedClientSessionModel clientSession,
                                                          String redirectUri, SAML2Object samlDocument,
                                                          JaxrsSAML2BindingBuilder bindingBuilder)
            throws ProcessingException, ConfigurationException {

        try {
            String artifact = buildArtifactAndStoreResponse(samlDocument, clientSession);
            String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);

            logger.debugf("Sending artifact %s to client %s", artifact, clientSession.getClient().getClientId());

            if (isPostBinding(clientSession)) {
                return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
            } else {
                return artifactRedirect(redirectUri, artifact, relayState);
            }

        } catch (ArtifactResolverProcessingException e) {
            throw new ProcessingException(e);
        }
    }

    /**
     * This method, instead of sending the actual response with the token, sends
     * the artifact message via post or redirect. This method is only to be used for the final LogoutResponse.
     *
     * @param userSession    The current user session being logged out
     * @param redirectUri    the redirect uri to the client
     * @param statusResponseType   a Document containing the saml Response
     * @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
     * @return A response (POSTed form or redirect) with a newly generated artifact
     * @throws ProcessingException
     * @throws IOException
     */
    protected Response buildLogoutArtifactResponse(UserSessionModel userSession,
                                                   String redirectUri, StatusResponseType statusResponseType,
                                                   JaxrsSAML2BindingBuilder bindingBuilder)
            throws ProcessingException, ConfigurationException {

        try {
            String artifact = buildArtifactAndStoreResponse(statusResponseType, userSession);
            String relayState = userSession.getNote(SAML_LOGOUT_RELAY_STATE);

            logger.debugf("Sending artifact for LogoutResponse %s to user %s", artifact, userSession.getLoginUsername());

            if (isLogoutPostBindingForInitiator(userSession)) {
                return artifactPost(redirectUri, artifact, relayState, bindingBuilder);
            } else {
                return artifactRedirect(redirectUri, artifact, relayState);
            }

        } catch (ArtifactResolverProcessingException e) {
            throw new ProcessingException(e);
        }
    }

    protected String buildArtifactAndStoreResponse(SAML2Object statusResponseType, UserSessionModel userSession) throws ArtifactResolverProcessingException, ConfigurationException, ProcessingException {
        String clientIdThatInitiatedLogout = userSession.getNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);
        userSession.removeNote(SAML_LOGOUT_INITIATOR_CLIENT_ID);

        AuthenticatedClientSessionModel clientSessionModel = userSession.getAuthenticatedClientSessionByClient(clientIdThatInitiatedLogout);
        if (clientSessionModel == null) {
            throw new IllegalStateException("Initiator client id is unknown when artifact response is created");
        }

        return buildArtifactAndStoreResponse(statusResponseType, clientSessionModel);
    }

    protected String buildArtifactAndStoreResponse(SAML2Object saml2Object, AuthenticatedClientSessionModel clientSessionModel) throws ArtifactResolverProcessingException, ProcessingException, ConfigurationException {
        String entityId = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
        ArtifactResponseType artifactResponseType = SamlProtocolUtils.buildArtifactResponse(saml2Object, SAML2NameIDBuilder.value(getResponseIssuer(realm)).build());


        // Create artifact and store session mapping
        SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
        String artifact = getArtifactResolver().buildArtifact(clientSessionModel, entityId, marshaller.serialize(artifactResponseType));
        HashMap<String, String> notes = new HashMap<>();
        notes.put(USER_SESSION_ID, clientSessionModel.getUserSession().getId());
        notes.put(CLIENT_SESSION_ID, clientSessionModel.getClient().getId());
        getSingleUseStore().put(artifact, realm.getAccessCodeLifespan(), notes);

        return artifact;
    }

    /**
     * Return an artifact through a redirect message
     *
     * @param redirectUri the redirect uri to the client
     * @param artifact    the artifact to send
     * @param relayState  the current relayState
     * @return a redirect Response with the artifact
     */
    private Response artifactRedirect(String redirectUri, String artifact, String relayState) {
        KeycloakUriBuilder builder = KeycloakUriBuilder.fromUri(redirectUri)
                .replaceQuery(null)
                .queryParam(GeneralConstants.SAML_ARTIFACT_KEY, artifact);

        if (relayState != null) {
            builder.queryParam(GeneralConstants.RELAY_STATE, relayState);
        }

        URI uri = builder.build();
        return Response.status(302).location(uri)
                .header("Pragma", "no-cache")
                .header("Cache-Control", "no-cache, no-store").build();
    }

    /**
     * Return an artifact through a POSTed form
     *
     * @param redirectUri    the redirect uri to the client
     * @param artifact       the artifact to send
     * @param relayState     current relayState
     * @param bindingBuilder the current JaxrsSAML2BindingBuilder configured with information for signing and encryption
     * @return a POSTed form response, with the artifact
     */
    private Response artifactPost(String redirectUri, String artifact, String relayState, JaxrsSAML2BindingBuilder bindingBuilder) {
        Map<String, String> inputTypes = new HashMap<>();
        inputTypes.put(GeneralConstants.SAML_ARTIFACT_KEY, artifact);
        if (relayState != null) {
            inputTypes.put(GeneralConstants.RELAY_STATE, relayState);
        }

        String str = bindingBuilder.buildHtmlForm(redirectUri, inputTypes);

        return Response.ok(str, MediaType.TEXT_HTML_TYPE)
                .header("Pragma", "no-cache")
                .header("Cache-Control", "no-cache, no-store").build();
    }

    /**
     * @param error mandatory parameter
     * @param errorDescription optional parameter
     */
    private record SAMLError(JBossSAMLURIConstants error, String errorDescription) {
    }

}
