/*
 * 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamWriter;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.container.AsyncResponse;
import jakarta.ws.rs.container.Suspended;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

import org.keycloak.broker.saml.SAMLDataMarshaller;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyStatus;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResponseType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
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.executors.ExecutorsProvider;
import org.keycloak.http.HttpRequest;
import org.keycloak.http.HttpResponse;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.AcrUtils;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor;
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.protocol.saml.util.ArtifactBindingUtils;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.BaseSAML2BindingBuilder;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAML2NameIDBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.core.saml.v2.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.saml.processing.core.saml.v2.util.XMLTimeUtil;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLRequestWriter;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.processing.web.util.PostBindingUtil;
import org.keycloak.saml.processing.web.util.RedirectBindingUtil;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.SamlAuthnRequestContext;
import org.keycloak.services.clientpolicy.context.SamlLogoutRequestContext;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.timer.ScheduledTask;
import org.keycloak.transaction.AsyncResponseTransaction;
import org.keycloak.utils.KeycloakSessionUtil;
import org.keycloak.utils.MediaType;

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.util.EntityUtils;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.NoCache;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import static org.keycloak.common.util.StackUtil.getShortStackTrace;

/**
 * Resource class for the saml connect token service
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class SamlService extends AuthorizationEndpointBase {

    protected static final Logger logger = Logger.getLogger(SamlService.class);
    public static final String ARTIFACT_RESOLUTION_SERVICE_PATH = "resolve";

    private final DestinationValidator destinationValidator;
    private final long maxInflatingSize;

    public SamlService(KeycloakSession session, EventBuilder event, long maxInflatingSize, DestinationValidator destinationValidator) {
        super(session, event);
        this.destinationValidator = destinationValidator;
        this.maxInflatingSize = maxInflatingSize;
    }

    public abstract class BindingProtocol {

        // this is to support back button on browser
        // if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
        // and we want to turn it off.
        protected boolean redirectToAuthentication;

        protected abstract Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters);

        protected Response sendProtocolError(AuthenticationSessionModel authSession, LoginProtocol.Error error, String errorMessage) {
            LoginProtocol protocol = session.getProvider(LoginProtocol.class, SamlProtocol.LOGIN_PROTOCOL);
            protocol.setRealm(realm)
                    .setHttpHeaders(session.getContext().getRequestHeaders())
                    .setUriInfo(session.getContext().getUri())
                    .setEventBuilder(event);
            return protocol.sendError(authSession, error, errorMessage);
        }

        protected Response basicChecks(String samlRequest, String samlResponse, String artifact) {
            logger.tracef("basicChecks(%s, %s, %s)%s", samlRequest, samlResponse, artifact, getShortStackTrace());
            if (!checkSsl()) {
                event.event(EventType.LOGIN);
                event.error(Errors.SSL_REQUIRED);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.HTTPS_REQUIRED);
            }
            if (!realm.isEnabled()) {
                event.event(EventType.LOGIN_ERROR);
                event.error(Errors.REALM_DISABLED);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.REALM_NOT_ENABLED);
            }

            if (samlRequest == null && samlResponse == null && artifact == null) {
                event.event(EventType.LOGIN);
                event.error(Errors.SAML_TOKEN_NOT_FOUND);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);

            }
            return null;
        }

        protected boolean isDestinationRequired() {
            return true;
        }

        protected Response handleSamlResponse(String samlResponse, String relayState) {
            event.event(EventType.LOGOUT);
            SAMLDocumentHolder holder = extractResponseDocument(samlResponse);

            if (holder == null || !(holder.getSamlObject() instanceof StatusResponseType)) {
                event.detail(Details.REASON, Errors.INVALID_SAML_RESPONSE);
                event.error(Errors.INVALID_SAML_RESPONSE);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
            // validate destination
            if (isDestinationRequired() &&
                    statusResponse.getDestination() == null && containsUnencryptedSignature(holder)) {
                event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
                event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }
            if (! destinationValidator.validate(this.getExpectedDestinationUri(session), statusResponse.getDestination())) {
                event.detail(Details.REASON, Errors.INVALID_DESTINATION);
                event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
            if (authResult == null) {
                logger.warn("Unknown saml response.");
                event.event(EventType.LOGOUT);
                event.error(Errors.INVALID_TOKEN);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }
            // assume this is a logout response
            UserSessionModel userSession = authResult.session();
            if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
                logger.warn("Unknown saml response.");
                logger.warn("UserSession is not tagged as logging out.");
                event.event(EventType.LOGOUT);
                event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }
            String issuer = statusResponse.getIssuer().getValue();
            ClientModel client = realm.getClientByClientId(issuer);
            if (client == null) {
                event.event(EventType.LOGOUT);
                event.client(issuer);
                event.error(Errors.CLIENT_NOT_FOUND);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
            }

            if (!isClientProtocolCorrect(client)) {
                event.event(EventType.LOGOUT);
                event.error(Errors.INVALID_CLIENT);
                return error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
            }

            session.getContext().setClient(client);
            logger.debug("logout response");
            Response response = authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
            event.success();
            return response;
        }

        protected Response triggerSamlEvent(ClientPolicyContext ctx) {
            try {
                session.clientPolicy().triggerOnEvent(ctx);
            } catch (ClientPolicyException cpe) {
                event.detail(Details.REASON, Details.CLIENT_POLICY_ERROR);
                event.detail(Details.CLIENT_POLICY_ERROR, cpe.getError());
                event.detail(Details.CLIENT_POLICY_ERROR_DETAIL, cpe.getErrorDetail());
                event.error(cpe.getError());
                logger.warnf("Error in client policies processing the request: %s - %s", cpe.getError(), cpe.getErrorDetail());
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }
            return null;
        }

        protected Response handleSamlRequest(String samlRequest, String relayState) {
            SAMLDocumentHolder documentHolder = extractRequestDocument(samlRequest);
            if (documentHolder == null) {
                event.event(EventType.LOGIN);
                event.error(Errors.INVALID_TOKEN);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }


            SAML2Object samlObject = documentHolder.getSamlObject();

            if (samlObject instanceof AuthnRequestType) {
                logger.debug("** login request");
                event.event(EventType.LOGIN);
            } else if (samlObject instanceof LogoutRequestType) {
                logger.debug("** logout request");
                event.event(EventType.LOGOUT);
            } else {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, "Unhandled SAML document type: " + (samlObject == null ? "<null>" : samlObject.getClass().getSimpleName()));
                event.error(Errors.INVALID_TOKEN);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
            final NameIDType issuerNameId = requestAbstractType.getIssuer();
            String issuer = requestAbstractType.getIssuer() == null ? null : issuerNameId.getValue();
            ClientModel client = realm.getClientByClientId(issuer);

            Response error = checkClientValidity(client);
            if (error != null) {
                return error;
            }

            session.getContext().setClient(client);

            SamlClient samlClient = new SamlClient(client);
            try {
                if (samlClient.requiresClientSignature()) {
                    verifySignature(documentHolder, client);
                }
            } catch (VerificationException e) {
                SamlService.logger.error("request validation failed", e);
                event.error(Errors.INVALID_SIGNATURE);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER);
            }
            logger.debug("verified request");

            if (isDestinationRequired() &&
                    requestAbstractType.getDestination() == null && containsUnencryptedSignature(documentHolder)) {
                event.detail(Details.REASON, Errors.MISSING_REQUIRED_DESTINATION);
                event.error(Errors.INVALID_REQUEST);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            if (samlObject instanceof AuthnRequestType) {
                // Get the SAML Request Message
                AuthnRequestType authn = (AuthnRequestType) samlObject;
                Response response = triggerSamlEvent(new SamlAuthnRequestContext(authn, client, getBindingType()));
                if (response != null) {
                    return response;
                }
                return loginRequest(relayState, authn, client);
            } else if (samlObject instanceof LogoutRequestType) {
                LogoutRequestType logout = (LogoutRequestType) samlObject;
                Response response = triggerSamlEvent(new SamlLogoutRequestContext(logout, client, getBindingType()));
                if (response != null) {
                    return response;
                }
                return logoutRequest(logout, client, relayState);
            } else {
                throw new IllegalStateException("Invalid SAML object");
            }
        }

        /**
         * Handle a received artifact message. This means finding the client based on the content of the artifact,
         * sending an ArtifactResolve, receiving an ArtifactResponse, and handling its content based on the "standard"
         * workflows.
         *
         * @param artifact the received artifact
         * @param relayState the current relay state
         * @return a Response based on the content of the ArtifactResponse's content
         */
        protected void handleArtifact(AsyncResponse asyncResponse, String artifact, String relayState) {
            logger.tracef("Keycloak obtained artifact %s. %s", artifact, getShortStackTrace());
            //Find client
            ClientModel client;
            try {
                client = getArtifactResolver(artifact).selectSourceClient(session, artifact);

                Response error = checkClientValidity(client);
                if (error != null) {
                    asyncResponse.resume(error);
                    return;
                }

            } catch (ArtifactResolverProcessingException | IllegalArgumentException e) {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, e.getMessage());
                event.error(Errors.INVALID_SAML_ARTIFACT);
                asyncResponse.resume(error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST));
                return;
            }

            try {
                //send artifact resolve
                Document doc = createArtifactResolve(client.getClientId(), artifact);
                BaseSAML2BindingBuilder binding = new BaseSAML2BindingBuilder();
                SamlClient samlClient = new SamlClient(client);
                if (samlClient.requiresRealmSignature()) {
                    KeyManager keyManager = session.keys();
                    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(doc);
                }
                String clientArtifactBindingURL = client.getAttribute(SamlProtocol.SAML_ARTIFACT_RESOLUTION_SERVICE_URL_ATTRIBUTE);

                if (clientArtifactBindingURL == null || clientArtifactBindingURL.isEmpty()) {
                    throw new ConfigurationException("There is no configured artifact resolution service for the client " + client.getClientId());
                }

                URI clientArtifactBindingURI = new URI(clientArtifactBindingURL);

                ExecutorService executor = session.getProvider(ExecutorsProvider.class).getExecutor("saml-artifact-pool");

                ArtifactResolutionRunnable artifactResolutionRunnable = new ArtifactResolutionRunnable(getBindingType(), asyncResponse, doc, clientArtifactBindingURI, relayState);
                ScheduledTaskRunner task = new ScheduledTaskRunner(session.getKeycloakSessionFactory(), artifactResolutionRunnable);
                executor.execute(task);

                logger.tracef("ArtifactResolutionRunnable scheduled, current transaction will be rolled back");
                // Current transaction must be ignored due to asyncResponse.
                session.getTransactionManager().rollback();
                // Remove the thread local - this thread will be returned, but KeycloakBeanProducer will only fire after the response is resumed.
                KeycloakSessionUtil.setKeycloakSession(null);
            } catch (URISyntaxException | ProcessingException | ParsingException | ConfigurationException e) {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, e.getMessage());
                event.error(Errors.IDENTITY_PROVIDER_ERROR);
                asyncResponse.resume(error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
                return;
            }
        }

        protected abstract String encodeSamlDocument(Document samlDocument) throws ProcessingException;

        protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;

        protected abstract boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder);

        protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);

        protected abstract SAMLDocumentHolder extractResponseDocument(String response);

        protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
            SamlClient samlClient = new SamlClient(client);

            if (! validateDestination(requestAbstractType, samlClient, Errors.INVALID_SAML_AUTHN_REQUEST)) {
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            String bindingType = getBindingType(requestAbstractType);
            if (samlClient.forcePostBinding())
                bindingType = SamlProtocol.SAML_POST_BINDING;
            String redirect;
            URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
            if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
                redirect = RedirectUtils.verifyRedirectUri(session, redirectUri.toString(), client);
            } else {
                if ((requestAbstractType.getProtocolBinding() != null
                        && JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
                            .equals(requestAbstractType.getProtocolBinding()))
                        || samlClient.forceArtifactBinding()) {
                    redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_ARTIFACT_ATTRIBUTE);
                } else if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
                    redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
                } else {
                    redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
                }
                if (redirect == null || redirect.trim().isEmpty()) {
                    redirect = client.getManagementUrl();
                }

            }

            if (redirect == null) {
                event.error(Errors.INVALID_REDIRECT_URI);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
            }

            AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);

            // determine if artifact binding should be used to answer the login request
            if ((requestAbstractType.getProtocolBinding() != null
                    && JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.getUri()
                        .equals(requestAbstractType.getProtocolBinding()))
                    || new SamlClient(client).forceArtifactBinding()) {
                authSession.setClientNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
            }

            authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
            authSession.setRedirectUri(redirect);
            authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
            authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType);
            authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
            authSession.setClientNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());

            // Handle NameIDPolicy from SP
            NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
            final URI nameIdFormatUri = nameIdPolicy == null ? null : nameIdPolicy.getFormat();
            if (nameIdFormatUri != null && ! samlClient.forceNameIDFormat()) {
                String nameIdFormat = nameIdFormatUri.toString();
                // TODO: Handle AllowCreate too, relevant for persistent NameID.
                if (isSupportedNameIdFormat(nameIdFormat)) {
                    authSession.setClientNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
                } else {
                    event.detail(Details.REASON, Errors.UNSUPPORTED_NAMEID_FORMAT);
                    event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
                    return error(session, null, Response.Status.BAD_REQUEST, Messages.UNSUPPORTED_NAME_ID_FORMAT);
                }
            }

            //Reading subject/nameID in the saml request
            SubjectType subject = requestAbstractType.getSubject();
            if (subject != null) {
                SubjectType.STSubType subType = subject.getSubType();
                if (subType != null) {
                    BaseIDAbstractType baseID = subject.getSubType().getBaseID();
                    if (baseID instanceof NameIDType) {
                        NameIDType nameID = (NameIDType) baseID;
                        authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
                    }
                }
            }

            if (null != requestAbstractType.isForceAuthn()
                    && requestAbstractType.isForceAuthn()) {
                authSession.setAuthNote(SamlProtocol.SAML_LOGIN_REQUEST_FORCEAUTHN, SamlProtocol.SAML_FORCEAUTHN_REQUIREMENT);
            }

            for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
                requestAbstractType = it.next().beforeProcessingLoginRequest(requestAbstractType, authSession);
            }

            if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION_SAML)) {
                // step-up level of authentication
                Map<String, Integer> acrLoaMap = AcrUtils.getUriLoaMap(authSession.getClient());

                if (!acrLoaMap.isEmpty()) {
                    // only process the requested authn context if LoA defined
                    String acrValue;
                    if (requestAbstractType.getRequestedAuthnContext() != null
                            && !requestAbstractType.getRequestedAuthnContext().getAuthnContextClassRef().isEmpty()) {
                        acrValue = SamlProtocolUtils.getSelectedLoA(requestAbstractType.getRequestedAuthnContext(),
                                acrLoaMap, AcrUtils.getMinimumAcrValue(client));
                        if (acrValue == null) {
                            logger.debug("No AuthnContextClassRef is valid for the requested context.");
                            event.detail(Details.REASON, "Invalid RequestedAuthnContext");
                            event.error(Errors.INVALID_REQUEST);
                            return sendProtocolError(authSession, LoginProtocol.Error.LOA_INVALID, null);
                        }
                    } else {
                        acrValue = AcrUtils.getMinimumAcrValue(client);
                    }

                    if (acrValue != null) {
                        logger.tracef("SAML step-up authentication set to force using context '%s'", acrValue);
                        authSession.setClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION, "true");
                        authSession.setClientNote(SamlProtocol.SAML_AUTHN_CONTEXT_CLASS_REF, acrValue);
                        authSession.setClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION, String.valueOf(acrLoaMap.get(acrValue)));
                    }
                }
            }

            //If unset we fall back to default "false"
            final boolean isPassive = (null != requestAbstractType.isIsPassive() && requestAbstractType.isIsPassive().booleanValue());
            return newBrowserAuthentication(authSession, isPassive, redirectToAuthentication);
        }

        protected String getBindingType(AuthnRequestType requestAbstractType) {
            URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();

            if (requestedProtocolBinding != null) {
                if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
                    return SamlProtocol.SAML_POST_BINDING;
                } else if (JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get().equals(requestedProtocolBinding.toString())) {
                    return getBindingType();
                } else {
                    return SamlProtocol.SAML_REDIRECT_BINDING;
                }
            }

            return getBindingType();
        }

        private boolean isSupportedNameIdFormat(String nameIdFormat) {
            if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())
                    || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
                return true;
            }
            return false;
        }

        protected abstract String getBindingType();

        protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
            SamlClient samlClient = new SamlClient(client);
            if (! validateDestination(logoutRequest, samlClient, Errors.INVALID_SAML_LOGOUT_REQUEST)) {
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }

            // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
            AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
            if (authResult != null) {
                String logoutBinding = getBindingType();
                String postBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, SamlProtocol.SAML_POST_BINDING, false);
                if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty())
                    logoutBinding = SamlProtocol.SAML_POST_BINDING;
                boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);

                String bindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, false);
                UserSessionModel userSession = authResult.session();
                userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
                if (samlClient.requiresRealmSignature()) {
                    userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, samlClient.getSignatureAlgorithm().toString());

                }
                if (relayState != null)
                    userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);

                userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
                userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
                userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
                userSession.setNote(SamlProtocol.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, samlClient.getXmlSigKeyInfoKeyNameTransformer().name());
                userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
                userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
                // remove client from logout requests
                AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
                if (clientSession != null) {
                    clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());

                    //artifact binding state must be attached to the user session upon logout, as authenticated session
                    //no longer exists when the LogoutResponse message is sent
                    if (SamlProtocol.SAML_ARTIFACT_BINDING.equals(SamlProtocol.getLogoutBindingTypeForClientSession(clientSession))){
                        clientSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
                        userSession.setNote(JBossSAMLURIConstants.SAML_HTTP_ARTIFACT_BINDING.get(), "true");
                        userSession.setNote(SamlProtocol.SAML_LOGOUT_INITIATOR_CLIENT_ID, client.getId());
                    }
                }

                for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
                    logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
                }

                logger.debug("browser Logout");
                return authManager.browserLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers);
            } else if (logoutRequest.getSessionIndex() != null) {
                for (String sessionIndex : logoutRequest.getSessionIndex()) {

                    AuthenticatedClientSessionModel clientSession = SamlSessionUtils.getClientSession(session, realm, sessionIndex);
                    if (clientSession == null)
                        continue;
                    UserSessionModel userSession = clientSession.getUserSession();
                    if (clientSession.getClient().getClientId().equals(client.getClientId())) {
                        // remove requesting client from logout
                        clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
                    }

                    for(Iterator<SamlAuthenticationPreprocessor> it = SamlSessionUtils.getSamlAuthenticationPreprocessorIterator(session); it.hasNext();) {
                        logoutRequest = it.next().beforeProcessingLogoutRequest(logoutRequest, userSession, clientSession);
                    }

                    try {
                        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, getBindingType());
                        authManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), clientConnection, headers, true);
                        event.success();
                    } catch (Exception e) {
                        logger.warn("Failure with backchannel logout", e);
                        event.error("Failure with backchannel logout");
                    }

                }

            }

            // default
            String logoutBinding = getBindingType();
            String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(session, client, logoutBinding, true);
            String logoutRelayState = relayState;
            SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
            builder.logoutRequestID(logoutRequest.getID());
            builder.destination(logoutBindingUri);
            builder.issuer(RealmsResource.realmBaseUrl(session.getContext().getUri()).build(realm.getName()).toString());
            JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder(session).relayState(logoutRelayState);
            boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
            if (samlClient.requiresRealmSignature()) {
                SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
                KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
                binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
                if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) {    // Only include extension if REDIRECT binding and signing whole SAML protocol message
                    builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
                }
            }
            try {
                if (postBinding) {
                    return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
                } else if (SamlProtocol.SAML_SOAP_BINDING.equals(logoutBinding)) {
                    return binding.soapBinding(builder.buildDocument()).response();
                } else {
                    return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private boolean validateDestination(RequestAbstractType req, SamlClient samlClient, String errorCode) {
            if (!isDestinationRequired() && req.getDestination() == null) {
                return true;
            }
            // validate destination
            if (req.getDestination() == null && samlClient.requiresClientSignature()) {
                event.detail(Details.REASON, "missing_destination_required");
                event.error(errorCode);
                return false;
            }
            if (! destinationValidator.validate(this.getExpectedDestinationUri(session), req.getDestination())) {
                event.detail(Details.REASON, Errors.INVALID_DESTINATION);
                event.error(errorCode);
                return false;
            }
            return true;
        }

        private boolean checkSsl() {
            if (session.getContext().getUri().getBaseUri().getScheme().equals("https")) {
                return true;
            } else {
                return !realm.getSslRequired().isRequired(clientConnection);
            }
        }

        public Response execute(String samlRequest, String samlResponse, String relayState, String artifact) {
            Response response = basicChecks(samlRequest, samlResponse, artifact);
            if (response != null)
                return response;

            if (samlRequest != null)
                return handleSamlRequest(samlRequest, relayState);
            else
                return handleSamlResponse(samlResponse, relayState);
        }

        public void execute(AsyncResponse asyncReponse, String samlRequest, String samlResponse, String relayState, String artifact) {
            Response response = basicChecks(samlRequest, samlResponse, artifact);

            if (response != null){
                asyncReponse.resume(response);
                return;
            }

            if (artifact != null) {
                handleArtifact(asyncReponse, artifact, relayState);
                return;
            }
            if (samlRequest != null) {
                asyncReponse.resume(handleSamlRequest(samlRequest, relayState));
                return;
            } else {
                asyncReponse.resume(handleSamlResponse(samlResponse, relayState));
            }
        }

        /**
         * KEYCLOAK-12616, KEYCLOAK-12944: construct the expected destination URI using the configured base URI.
         *
         * @param session a reference to the {@link KeycloakSession}.
         * @return the constructed {@link URI}.
         */
        protected URI getExpectedDestinationUri(final KeycloakSession session) {
            final String realmName = session.getContext().getRealm().getName();
            final URI baseUri = session.getContext().getUri().getBaseUri();
            return Urls.samlRequestEndpoint(baseUri, realmName);
        }

        private Response checkClientValidity(ClientModel client) {
            if (client == null) {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, "Cannot_match_source_hash");
                event.error(Errors.CLIENT_NOT_FOUND);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
            }
            if (!client.isEnabled()) {
                event.event(EventType.LOGIN);
                event.error(Errors.CLIENT_DISABLED);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.LOGIN_REQUESTER_NOT_ENABLED);
            }
            if (client.isBearerOnly()) {
                event.event(EventType.LOGIN);
                event.error(Errors.NOT_ALLOWED);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.BEARER_ONLY);
            }
            if (!client.isStandardFlowEnabled()) {
                event.event(EventType.LOGIN);
                event.error(Errors.NOT_ALLOWED);
                return error(session, null, Response.Status.BAD_REQUEST, Messages.STANDARD_FLOW_DISABLED);
            }
            if (!isClientProtocolCorrect(client)) {
                event.event(EventType.LOGIN);
                event.error(Errors.INVALID_CLIENT);
                return error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
            }

            return null;
        }
    }

    protected class PostBindingProtocol extends BindingProtocol {

        @Override
        protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
            return ErrorPage.error(session, authenticationSession, status, message, parameters);
        }

        @Override
        protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
            try {
                return PostBindingUtil.base64Encode(DocumentUtil.asString(samlDocument));
            } catch (IOException e) {
                throw new ProcessingException(e);
            }
        }

        @Override
        protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
            SamlProtocolUtils.verifyDocumentSignature(session, client, documentHolder.getSamlDocument());
        }

        @Override
        protected boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder) {
            Document signedDoc = documentHolder.getSamlDocument();
            NodeList nl = signedDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
            return nl != null && nl.getLength() > 0;
        }

        @Override
        protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
            return SAMLRequestParser.parseRequestPostBinding(samlRequest);
        }

        @Override
        protected SAMLDocumentHolder extractResponseDocument(String response) {
            return SAMLRequestParser.parseResponsePostBinding(response);
        }

        @Override
        protected String getBindingType() {
            return SamlProtocol.SAML_POST_BINDING;
        }

    }

    protected class RedirectBindingProtocol extends BindingProtocol {

        @Override
        protected Response error(KeycloakSession session, AuthenticationSessionModel authenticationSession, Response.Status status, String message, Object... parameters) {
            return ErrorPage.error(session, authenticationSession, status, message, parameters);
        }

        @Override
        protected String encodeSamlDocument(Document samlDocument) throws ProcessingException {
            try {
                return RedirectBindingUtil.deflateBase64Encode(DocumentUtil.asString(samlDocument).getBytes(GeneralConstants.SAML_CHARSET_NAME));
            } catch (IOException e) {
                throw new ProcessingException(e);
            }
        }

        @Override
        protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
            KeyLocator clientKeyLocator = SamlProtocolUtils.createKeyLocatorForClient(session, client, KeyUse.SIG);
            SamlProtocolUtils.verifyRedirectSignature(documentHolder, clientKeyLocator, session.getContext().getUri(), GeneralConstants.SAML_REQUEST_KEY);
        }

        @Override
        protected boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder) {
            KeycloakUriInfo uriInformation = session.getContext().getUri();
            MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
            String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
            return algorithm != null;
        }

        @Override
        protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
            return SAMLRequestParser.parseRequestRedirectBinding(samlRequest, maxInflatingSize);
        }

        @Override
        protected SAMLDocumentHolder extractResponseDocument(String response) {
            return SAMLRequestParser.parseResponseRedirectBinding(response, maxInflatingSize);
        }

        @Override
        protected String getBindingType() {
            return SamlProtocol.SAML_REDIRECT_BINDING;
        }

    }

    protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication) {
        SamlProtocol samlProtocol = new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(session.getContext().getUri());
        return newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, samlProtocol);
    }

    protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
        return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
    }

    public RedirectBindingProtocol newRedirectBindingProtocol() {
        return new RedirectBindingProtocol();
    }

    public PostBindingProtocol newPostBindingProtocol() {
        return new PostBindingProtocol();
    }

    /**
     */
    @GET
    public void redirectBinding(@Suspended AsyncResponse asyncResponse, @QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState, @QueryParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
        logger.debug("SAML GET");
        CacheControlUtil.noBackButtonCacheControlHeader(session);

        new RedirectBindingProtocol().execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
    }

    /**
     */
    @POST
    @NoCache
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void postBinding(@Suspended AsyncResponse asyncResponse, @FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @FormParam(GeneralConstants.RELAY_STATE) String relayState, @FormParam(GeneralConstants.SAML_ARTIFACT_KEY) String artifact) {
        logger.debug("SAML POST");
        PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
        // this is to support back button on browser
        // if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
        // and we want to turn it off.
        postBindingProtocol.redirectToAuthentication = true;
        postBindingProtocol.execute(asyncResponse, samlRequest, samlResponse, relayState, artifact);
    }

    @GET
    @Path("descriptor")
    @Produces(MediaType.APPLICATION_XML)
    @NoCache
    public String getDescriptor() throws Exception {
        return getIDPMetadataDescriptor(session.getContext().getUri(), session, realm);

    }

    public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) {
        try {
            List<KeyWrapper> keys = session.keys().getKeysStream(realm, KeyUse.SIG, Algorithm.RS256)
                    .sorted(SamlService::compareKeys)
                    .collect(Collectors.toList());
            List<Element> signingKeys = keys.stream()
                    .map(key -> {
                        try {
                            return IDPMetadataDescriptor
                                    .buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate()));
                        } catch (ParserConfigurationException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .collect(Collectors.toList());

            return IDPMetadataDescriptor.getIDPDescriptor(
                keys.isEmpty()? null : keys.iterator().next(),
                realm.getAttribute(SamlConfigAttributes.SAML_SIGNATURE_ALGORITHM, SignatureAlgorithm.class, null),
                RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
                RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
                RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
                RealmsResource.protocolUrl(uriInfo).path(SamlService.ARTIFACT_RESOLUTION_SERVICE_PATH)
                        .build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
                RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(),
                true,
                signingKeys,
                realm.getAttribute(SamlConfigAttributes.SAML_DESCRIPTOR_CACHE_SECONDS, (Long) null));
        } catch (Exception ex) {
            logger.error("Cannot generate IdP metadata", ex);
            return "";
        }
    }

    public static int compareKeys(KeyWrapper o1, KeyWrapper o2) {
        return o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
                ? (int) (o2.getProviderPriority() - o1.getProviderPriority())
                : (o1.getStatus() == KeyStatus.PASSIVE ? 1 : -1);
    }

    private boolean isClientProtocolCorrect(ClientModel clientModel) {
        if (SamlProtocol.LOGIN_PROTOCOL.equals(clientModel.getProtocol())) {
            return true;
        }

        return false;
    }

    @GET
    @Path("clients/{client}")
    @Produces(MediaType.TEXT_HTML_UTF_8)
    public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) {
        event.event(EventType.LOGIN);
        CacheControlUtil.noBackButtonCacheControlHeader(session);
        ClientModel client = session.clients()
                .searchClientsByAttributes(realm, Collections.singletonMap(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME, clientUrlName), 0, 1)
                .findFirst().orElse(null);

        if (client == null) {
            event.error(Errors.CLIENT_NOT_FOUND);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
        }
        if (!client.isEnabled()) {
            event.error(Errors.CLIENT_DISABLED);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED);
        }
        if (!isClientProtocolCorrect(client)) {
            event.error(Errors.INVALID_CLIENT);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, "Wrong client protocol.");
        }

        session.getContext().setClient(client);

        AuthenticationSessionModel authSession = getOrCreateLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
        if (authSession == null) {
            logger.error("SAML assertion consumer url not set up");
            event.error(Errors.INVALID_REDIRECT_URI);
            return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
        }

        return newBrowserAuthentication(authSession, false, false);
    }

    /**
     * Checks the client configuration to return the redirect URL and the binding type.
     * POST is preferred, only if the SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE
     * and management URL are empty REDIRECT is chosen.
     *
     * @param client Client to create client session for
     * @return a two string array [samlUrl, bindingType] or null if error
     */
    private String[] getUrlAndBindingForIdpInitiatedSso(ClientModel client) {
        String postUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
        String getUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
        if (postUrl != null && !postUrl.trim().isEmpty()) {
            // first the POST binding URL
            return new String[] {postUrl.trim(), SamlProtocol.SAML_POST_BINDING};
        } else if (client.getManagementUrl() != null && !client.getManagementUrl().trim().isEmpty()) {
            // second the management URL and POST
            return new String[] {client.getManagementUrl().trim(), SamlProtocol.SAML_POST_BINDING};
        } else if (getUrl != null && !getUrl.trim().isEmpty()){
            // last option REDIRECT binding and URL
            return new String[] {getUrl.trim(), SamlProtocol.SAML_REDIRECT_BINDING};
        } else {
            // error
            return null;
        }
    }

    /**
     * Creates a client session object for SAML IdP-initiated SSO session.
     * The session takes the parameters from from client definition,
     * namely binding type and redirect URL.
     *
     * @param session KC session
     * @param realm Realm to create client session in
     * @param client Client to create client session for
     * @param relayState Optional relay state - free field as per SAML specification
     * @return The auth session model or null if there is no SAML url is found
     */
    public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
        String[] bindingProperties = getUrlAndBindingForIdpInitiatedSso(client);
        if (bindingProperties == null) {
            return null;
        }
        String redirect = bindingProperties[0];
        String bindingType = bindingProperties[1];

        AuthenticationSessionModel authSession = createAuthenticationSession(client, null);

        authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
        authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
        authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType);
        authSession.setClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
        authSession.setRedirectUri(redirect);

        if (relayState == null) {
            relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE);
        }
        if (relayState != null && !relayState.trim().equals("")) {
            authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
        }

        return authSession;
    }

    /**
     * Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP or Artifact
     * @param inputStream the data of the request.
     * @return The response to the SOAP message
     */
    @POST
    @Path(ARTIFACT_RESOLUTION_SERVICE_PATH)
    @NoCache
    @Consumes({"application/soap+xml", MediaType.TEXT_XML})
    public Response artifactResolutionService(InputStream inputStream) {
        Document soapBodyContents = Soap.extractSoapMessage(inputStream);
        ArtifactResolveType artifactResolveType = null;
        SAMLDocumentHolder samlDocumentHolder = null;
        try {
            samlDocumentHolder = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
            if (samlDocumentHolder.getSamlObject() instanceof ArtifactResolveType) {
                logger.debug("Received artifact resolve message");
                artifactResolveType = (ArtifactResolveType)samlDocumentHolder.getSamlObject();
            }
        } catch (Exception e) {
            logger.errorf("Artifact resolution endpoint obtained request that contained no " +
                    "ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
            return Soap.createFault().reason("").detail("").build();
        }

        if (artifactResolveType == null) {
            logger.errorf("Artifact resolution endpoint obtained request that contained no " +
                    "ArtifactResolve message: %s", DocumentUtil.asString(soapBodyContents));
            return Soap.createFault().reason("").detail("").build();
        }

        try {
            return artifactResolve(artifactResolveType, samlDocumentHolder);
        } catch (Exception e) {
            try {
                return emptyArtifactResponseMessage(artifactResolveType, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
            } catch (ConfigurationException | ProcessingException configurationException) {
                String reason = "An error occurred while trying to return the artifactResponse";
                String detail = e.getMessage();

                if (detail == null) {
                    detail = "";
                }

                logger.errorf("Failure during ArtifactResolve reason: %s, detail: %s", reason, detail);
                return Soap.createFault().reason(reason).detail(detail).build();
            }
        }
    }


    /**
     * Handles SOAP messages. Chooses the correct response path depending on whether the message is of type ECP
     * @param inputStream the data of the request.
     * @return The response to the SOAP message
     */
    @POST
    @NoCache
    @Consumes({"application/soap+xml",MediaType.TEXT_XML})
    public Response soapBinding(InputStream inputStream) {
        SamlEcpProfileService bindingService = new SamlEcpProfileService(session, event, maxInflatingSize, destinationValidator);

        return bindingService.authenticate(inputStream);
    }

    private ClientModel getAndCheckClientModel(String clientSessionId, String clientId) throws ProcessingException {
        ClientModel client = session.clients().getClientById(realm, clientSessionId);

        if (client == null) {
            throw new ProcessingException(Errors.CLIENT_NOT_FOUND);
        }
        if (!client.isEnabled()) {
            throw new ProcessingException(Errors.CLIENT_DISABLED);
        }
        if (client.isBearerOnly()) {
            throw new ProcessingException(Errors.NOT_ALLOWED);
        }
        if (!client.isStandardFlowEnabled()) {
            throw new ProcessingException(Errors.NOT_ALLOWED);
        }
        if (!client.getClientId().equals(clientId)) {
            logger.errorf("Resolve message with wrong issuer. Artifact was issued for client %s, " +
                            "however ArtifactResolveMessage came from client %s.", client.getClientId(), clientId);
            throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT);
        }

        return client;
    }


    private SingleUseObjectProvider getSingleUseStore() {
        return session.singleUseObjects();
    }

    /**
     * Takes an artifact resolve message and returns the artifact response, if the artifact is found belonging to a session
     * of the issuer.
     * @param artifactResolveMessage The artifact resolve message sent by the client
     * @param artifactResolveHolder the document containing the artifact resolve message sent by the client
     * @return a Response containing the SOAP message with the ArifactResponse
     * @throws ParsingException
     * @throws ConfigurationException
     * @throws ProcessingException
     */
    public Response artifactResolve(ArtifactResolveType artifactResolveMessage, SAMLDocumentHolder artifactResolveHolder) throws ParsingException, ConfigurationException, ProcessingException {
        logger.debugf("Received artifactResolve message for artifact %s\n" +
                "Message: \n%s", artifactResolveMessage.getArtifact(), DocumentUtil.getDocumentAsString(artifactResolveHolder.getSamlDocument()));

        String artifact = artifactResolveMessage.getArtifact(); // Artifact from resolve request
        if (artifact == null) {
            logger.errorf("Artifact to resolve was null");
            return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
        }

        ArtifactResolver artifactResolver;
        try {
            artifactResolver = getArtifactResolver(artifact);
        } catch (IllegalArgumentException e) {
            logger.errorf("Invalid artifact format: %s", artifact);
            return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
        }

        if (artifactResolver == null) {
            logger.errorf("Cannot find ArtifactResolver for artifact %s", artifact);
            return emptyArtifactResponseMessage(artifactResolveMessage, null, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.getUri());
        }

        // Obtain details of session that issued artifact and check if it corresponds to issuer of Resolve message
        Map<String, String> sessionMapping = getSingleUseStore().get(artifact);

        if (sessionMapping == null) {
            logger.errorf("No data stored for artifact %s", artifact);
            return emptyArtifactResponseMessage(artifactResolveMessage, null);
        }

        UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionMapping.get(SamlProtocol.USER_SESSION_ID));
        if (userSessionModel == null) {
            logger.errorf("UserSession with id: %s, that corresponds to artifact: %s does not exist.", sessionMapping.get(SamlProtocol.USER_SESSION_ID), artifact);
            return emptyArtifactResponseMessage(artifactResolveMessage, null);
        }

        AuthenticatedClientSessionModel clientSessionModel = userSessionModel.getAuthenticatedClientSessionByClient(sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID));
        if (clientSessionModel == null) {
            logger.errorf("ClientSession with id: %s, that corresponds to artifact: %s and UserSession: %s does not exist.",
                    sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID), artifact, sessionMapping.get(SamlProtocol.USER_SESSION_ID));
            return emptyArtifactResponseMessage(artifactResolveMessage, null);
        }

        ClientModel clientModel = getAndCheckClientModel(sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID), artifactResolveMessage.getIssuer().getValue());
        SamlClient samlClient = new SamlClient(clientModel);

        // Check signature within ArtifactResolve request if client requires it
        if (samlClient.requiresClientSignature()) {
            try {
                SamlProtocolUtils.verifyDocumentSignature(session, clientModel, artifactResolveHolder.getSamlDocument());
            } catch (VerificationException e) {
                SamlService.logger.error("request validation failed", e);
                return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
            }
        }

        // Obtain artifactResponse from clientSessionModel
        String artifactResponseString;
        try {
            artifactResponseString = artifactResolver.resolveArtifact(clientSessionModel, artifact);
        } catch (ArtifactResolverProcessingException e) {
            logger.errorf(e, "Failed to resolve artifact: %s.", artifact);
            return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
        }

        // Artifact is successfully resolved, we can remove session mapping from storage
        if (getSingleUseStore().remove(artifact) == null) {
            logger.debugf("Artifact %s was already removed", artifact);
        }

        Document artifactResponseDocument = null;
        ArtifactResponseType artifactResponseType = null;
        try {
            SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
            artifactResponseType = marshaller.deserialize(artifactResponseString, ArtifactResponseType.class);
            artifactResponseDocument = SamlProtocolUtils.convert(artifactResponseType);
        }  catch (ParsingException | ConfigurationException | ProcessingException e) {
            logger.errorf(e,"Failed to obtain document from ArtifactResponseString: %s.", artifactResponseString);
            return emptyArtifactResponseMessage(artifactResolveMessage, clientModel);
        }

        // If clientSession is in LOGGING_OUT action, now we can move it to LOGGED_OUT
        if (CommonClientSessionModel.Action.LOGGING_OUT.name().equals(clientSessionModel.getAction())) {
            clientSessionModel.setAction(CommonClientSessionModel.Action.LOGGED_OUT.name());

            // If Keycloak sent LogoutResponse we need to also remove UserSession
            if (artifactResponseType.getAny() instanceof StatusResponseType
                    && artifactResponseString.contains(JBossSAMLConstants.LOGOUT_RESPONSE.get())) {
                if (!UserSessionModel.State.LOGGED_OUT_UNCONFIRMED.equals(userSessionModel.getState())) {
                    logger.warnf("Keycloak issued LogoutResponse for clientSession %s, however user session %s was not in LOGGED_OUT_UNCONFIRMED state.",
                            clientSessionModel.getId(), userSessionModel.getId());
                }
                AuthenticationManager.finishUnconfirmedUserSession(session, realm, userSessionModel);
            }
        }

        return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
    }

    private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel) throws ProcessingException, ConfigurationException {
        return emptyArtifactResponseMessage(artifactResolveMessage, clientModel, JBossSAMLURIConstants.STATUS_SUCCESS.getUri());
    }

    private Response emptyArtifactResponseMessage(ArtifactResolveType artifactResolveMessage, ClientModel clientModel, URI responseStatusCode) throws ProcessingException, ConfigurationException {
        ArtifactResponseType artifactResponse = SamlProtocolUtils.buildArtifactResponse(null, SAML2NameIDBuilder.value(
                RealmsResource.realmBaseUrl(session.getContext().getUri()).build(realm.getName()).toString()).build(), responseStatusCode);

        Document artifactResponseDocument;
        try {
            artifactResponseDocument = SamlProtocolUtils.convert(artifactResponse);
        }  catch (ParsingException | ConfigurationException | ProcessingException e) {
            logger.errorf("Failed to obtain document from ArtifactResponse: %s.", artifactResponse);
            throw new ProcessingException(Errors.INVALID_SAML_ARTIFACT_RESPONSE, e);
        }

        return artifactResponseMessage(artifactResolveMessage, artifactResponseDocument, clientModel);
    }

    private Response artifactResponseMessage(ArtifactResolveType artifactResolveMessage, Document artifactResponseDocument, ClientModel clientModel) throws ProcessingException, ConfigurationException {
        // Add "inResponseTo" to artifactResponse
        if (artifactResolveMessage.getID() != null && !artifactResolveMessage.getID().trim().isEmpty()){
            Element artifactResponseElement = artifactResponseDocument.getDocumentElement();
            artifactResponseElement.setAttribute("InResponseTo", artifactResolveMessage.getID());
        }
        JaxrsSAML2BindingBuilder bindingBuilder = new JaxrsSAML2BindingBuilder(session);

        if (clientModel != null) {
            SamlClient samlClient = new SamlClient(clientModel);

            // Sign document/assertion if necessary, necessary to do this here, as the "inResponseTo" can only be set at this point
            if (samlClient.requiresRealmSignature() || samlClient.requiresAssertionSignature()) {
                KeyManager keyManager = session.keys();
                KeyManager.ActiveRsaKey keys = keyManager.getActiveRsaKey(realm);
                String keyName = samlClient.getXmlSigKeyInfoKeyNameTransformer().getKeyName(keys.getKid(), keys.getCertificate());
                String canonicalization = samlClient.getCanonicalizationMethod();
                if (canonicalization != null) {
                    bindingBuilder.canonicalizationMethod(canonicalization);
                }
                bindingBuilder.signatureAlgorithm(samlClient.getSignatureAlgorithm()).signWith(keyName, keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate());

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

            // Encrypt assertion if client requires it
            if (samlClient.requiresEncryption()) {
                try {
                    SamlProtocolUtils.setupEncryption(session, samlClient, bindingBuilder);
                } catch (Exception e) {
                    logger.error("Failed to obtain encryption key for client", e);
                    return emptyArtifactResponseMessage(artifactResolveMessage, null);
                }
            }
        }

        bindingBuilder.postBinding(artifactResponseDocument);

        Soap.SoapMessageBuilder messageBuilder = Soap.createMessage();
        messageBuilder.addToBody(artifactResponseDocument);

        if (logger.isDebugEnabled()) {
            String artifactResponse = DocumentUtil.asString(artifactResponseDocument);
            logger.debugf("Sending artifactResponse message for artifact %s. Message: \n %s", artifactResolveMessage.getArtifact(), artifactResponse);
        }

        return messageBuilder.build();
    }

    /**
     * Creates an ArtifactResolve document with the given issuer and artifact
     * @param issuer the value to set as "issuer"
     * @param artifact the value to set as "artifact"
     * @return the Document of the created ArtifactResolve message
     * @throws ProcessingException
     * @throws ParsingException
     * @throws ConfigurationException
     */
    private Document createArtifactResolve(String issuer, String artifact) throws ProcessingException, ParsingException, ConfigurationException {
        ArtifactResolveType artifactResolve = new ArtifactResolveType(IDGenerator.create("ID_"),
                XMLTimeUtil.getIssueInstant());
        NameIDType nameIDType = new NameIDType();
        nameIDType.setValue(issuer);
        artifactResolve.setIssuer(nameIDType);
        artifactResolve.setArtifact(artifact);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        XMLStreamWriter xmlStreamWriter = StaxUtil.getXMLStreamWriter(bos);
        new SAMLRequestWriter(xmlStreamWriter).write(artifactResolve);
        return DocumentUtil.getDocument(new ByteArrayInputStream(bos.toByteArray()));
    }

    private ArtifactResolver getArtifactResolver(String artifact) {
        ArtifactResolver artifactResolver = session.getProvider(ArtifactResolver.class, ArtifactBindingUtils.artifactToResolverProviderId(artifact));
        return artifactResolver != null ? artifactResolver : session.getProvider(ArtifactResolver.class);
    }

    private class ArtifactResolutionRunnable implements ScheduledTask{

        private final HttpRequest request;
        private final HttpResponse response;
        private AsyncResponse asyncResponse;
        private URI clientArtifactBindingURI;
        private String relayState;
        private Document doc;
        private String realmId;
        private ClientConnection connection;
        private String bindingType;

        public ArtifactResolutionRunnable(String bindingType, AsyncResponse asyncResponse, Document doc, URI clientArtifactBindingURI, String relayState){
            this.asyncResponse = asyncResponse;
            this.doc = doc;
            this.clientArtifactBindingURI = clientArtifactBindingURI;
            this.relayState = relayState;
            this.realmId = realm.getId();
            this.connection = session.getContext().getConnection();
            this.bindingType = bindingType;
            this.request = session.getContext().getHttpRequest();
            this.response = session.getContext().getHttpResponse();
        }

        @Override
        public void run(KeycloakSession session){
            // Initialize context
            session.getContext().setHttpRequest(request);
            session.getContext().setHttpResponse(response);
            session.getContext().setConnection(connection);

            RealmManager realmManager = new RealmManager(session);
            RealmModel realm = realmManager.getRealm(realmId);
            if (realm == null) {
                throw new NotFoundException("Realm does not exist");
            }
            session.getContext().setRealm(realm);

            EventBuilder event = new EventBuilder(realm, session, clientConnection);

            // Call Artifact Resolution Service
            HttpClientProvider httpClientProvider = session.getProvider(HttpClientProvider.class);
            CloseableHttpClient httpClient = httpClientProvider.getHttpClient();
            HttpPost httpPost = Soap.createMessage().addToBody(doc).buildHttpPost(clientArtifactBindingURI);

            if (logger.isTraceEnabled()) {
                logger.tracef("Resolving artifact %s", DocumentUtil.asString(doc));
            }

            try (CloseableHttpResponse result = httpClient.execute(httpPost)) {
                try {
                    if (result.getStatusLine().getStatusCode() != Response.Status.OK.getStatusCode()) {
                        throw new ProcessingException(String.format("Artifact resolution failed with status: %d", result.getStatusLine().getStatusCode()));
                    }

                    Document soapBodyContents = Soap.extractSoapMessage(result.getEntity().getContent());
                    SAMLDocumentHolder samlDoc = SAML2Request.getSAML2ObjectFromDocument(soapBodyContents);
                    if (!(samlDoc.getSamlObject() instanceof ArtifactResponseType)) {
                        throw new ProcessingException("Message received from ArtifactResolveService is not an ArtifactResponseMessage");
                    }

                    if (logger.isTraceEnabled()) {
                        logger.tracef("Resolved object: %s" + DocumentUtil.asString(samlDoc.getSamlDocument()));
                    }

                    ArtifactResponseType art = (ArtifactResponseType) samlDoc.getSamlObject();

                    if (art.getAny() == null) {
                        AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
                                ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
                        return;
                    }

                    LoginProtocolFactory factory = (LoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, "saml");
                    if (factory == null) {
                        logger.debugf("protocol %s not found", "saml");
                        throw new NotFoundException("Protocol not found");
                    }

                    SamlService endpoint = (SamlService) factory.createProtocolEndpoint(session, event);
                    BindingProtocol protocol;
                    if (SamlProtocol.SAML_POST_BINDING.equals(bindingType)) {
                        protocol = endpoint.newPostBindingProtocol();
                    } else if (SamlProtocol.SAML_REDIRECT_BINDING.equals(bindingType)) {
                        protocol = endpoint.newRedirectBindingProtocol();
                    } else {
                        throw new ConfigurationException("Invalid binding protocol: " + bindingType);
                    }

                    if (art.getAny() instanceof ResponseType) {
                        Document clientMessage = SAML2Request.convert((ResponseType) art.getAny());
                        String response = protocol.encodeSamlDocument(clientMessage);

                        AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
                                protocol.handleSamlResponse(response, relayState));
                    } else if (art.getAny() instanceof RequestAbstractType) {
                        Document clientMessage = SAML2Request.convert((RequestAbstractType) art.getAny());
                        String request = protocol.encodeSamlDocument(clientMessage);
                        AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
                                protocol.handleSamlRequest(request, relayState));
                    } else {
                        throw new ProcessingException("Cannot recognise message contained in ArtifactResponse");
                    }

                } finally {
                    EntityUtils.consumeQuietly(result.getEntity());
                }

            } catch (IOException | ProcessingException | ParsingException | IllegalArgumentException e) {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, e.getMessage());
                event.error(Errors.IDENTITY_PROVIDER_ERROR);

                AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
                        ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.ARTIFACT_RESOLUTION_SERVICE_INVALID_RESPONSE));
            } catch(ConfigurationException e) {
                event.event(EventType.LOGIN);
                event.detail(Details.REASON, e.getMessage());
                event.error(Errors.IDENTITY_PROVIDER_ERROR);
                AsyncResponseTransaction.finishAsyncResponseInTransaction(session, asyncResponse,
                        ErrorPage.error(session, null, Response.Status.INTERNAL_SERVER_ERROR, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST));
            }
        }
    }

}
