All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pac4j.saml.logout.impl.SAML2LogoutValidator Maven / Gradle / Ivy

package org.pac4j.saml.logout.impl;

import net.shibboleth.shared.net.URIComparator;
import org.opensaml.saml.common.SAMLObject;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.LogoutRequest;
import org.opensaml.saml.saml2.core.LogoutResponse;
import org.opensaml.saml.saml2.core.Status;
import org.opensaml.saml.saml2.core.StatusCode;
import org.opensaml.saml.saml2.encryption.Decrypter;
import org.opensaml.saml.saml2.metadata.Endpoint;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.exception.http.FoundAction;
import org.pac4j.core.exception.http.HttpAction;
import org.pac4j.core.exception.http.OkAction;
import org.pac4j.core.logout.handler.LogoutHandler;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.core.util.Pac4jConstants;
import org.pac4j.saml.context.SAML2MessageContext;
import org.pac4j.saml.credentials.SAML2Credentials;
import org.pac4j.saml.crypto.SAML2SignatureTrustEngineProvider;
import org.pac4j.saml.exceptions.SAMLException;
import org.pac4j.saml.profile.impl.AbstractSAML2ResponseValidator;
import org.pac4j.saml.replay.ReplayCacheProvider;
import org.pac4j.saml.util.Configuration;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Validator for SAML logout requests/responses from the IdP.
 *
 * @author Matthieu Taggiasco
 * @author Jerome Leleu
 * @since 2.0.0
 */
public class SAML2LogoutValidator extends AbstractSAML2ResponseValidator {

    private String postLogoutURL;

    /**
     * When set to false, will cause the validator
     * to not throw back exceptions and expect adaptations of those exceptions
     * when the response is successfully validated. Instead, the validator should successfully
     * move on without throwing {@link OkAction}.
     */
    private boolean actionOnSuccess = true;

    /**
     * Logouts are only successful if the IdP was able to inform all services, otherwise it will
     * respond with PartialLogout. This setting allows clients to ignore such server-side problems.
     */
    private boolean isPartialLogoutTreatedAsSuccess = false;

    /**
     * Expected destination endpoint when validating saml2 logout responses.
     * If left blank, will use the SLO endpoint from metadata context.
     */
    private String expectedDestination;

    public SAML2LogoutValidator(final SAML2SignatureTrustEngineProvider engine, final Decrypter decrypter,
                                final LogoutHandler logoutHandler, final String postLogoutURL,
                                final ReplayCacheProvider replayCache, final URIComparator uriComparator) {
        super(engine, decrypter, logoutHandler, replayCache, uriComparator);
        this.postLogoutURL = postLogoutURL;
    }

    /**
     * Validates the SAML protocol logout request/response.
     *
     * @param context the context
     */
    @Override
    public Credentials validate(final SAML2MessageContext context) {
        final var message = (SAMLObject) context.getMessageContext().getMessage();
        // IDP-initiated
        if (message instanceof LogoutRequest) {
            final var logoutRequest = (LogoutRequest) message;
            final var engine = this.signatureTrustEngineProvider.build();
            validateLogoutRequest(logoutRequest, context, engine);
            return null;
        } else if (message instanceof LogoutResponse) {
            // SP-initiated
            final var logoutResponse = (LogoutResponse) message;
            final var engine = this.signatureTrustEngineProvider.build();
            validateLogoutResponse(logoutResponse, context, engine);

            final var action = handlePostLogoutResponse(context);
            if (action != null) {
                throw action;
            }
            return null;
        }
        throw new SAMLException("SAML message must be a LogoutRequest or LogoutResponse type");
    }

    protected HttpAction handlePostLogoutResponse(final SAML2MessageContext context) {
        if (CommonHelper.isNotBlank(postLogoutURL)) {
            // if custom post logout URL is present then redirect to it
            return new FoundAction(postLogoutURL);
        }
        // nothing to reply to the logout response
        return this.actionOnSuccess ? new OkAction(Pac4jConstants.EMPTY_STRING) : null;
    }

    /**
     * Validates the SAML logout request.
     *
     * @param logoutRequest the logout request
     * @param context       the context
     * @param engine        the signature engine
     */
    protected void validateLogoutRequest(final LogoutRequest logoutRequest, final SAML2MessageContext context,
                                         final SignatureTrustEngine engine) {

        if (logger.isTraceEnabled()) {
            logger.trace("Validating logout request:\n{}", Configuration.serializeSamlObject(logoutRequest));
        }
        validateSignatureIfItExists(logoutRequest.getSignature(), context, engine);

        // don't check because of CAS v5
        //validateIssueInstant(logoutRequest.getIssueInstant());

        validateIssuerIfItExists(logoutRequest.getIssuer(), context);

        var nameId = logoutRequest.getNameID();
        final var encryptedID = logoutRequest.getEncryptedID();
        if (encryptedID != null) {
            nameId = decryptEncryptedId(encryptedID, decrypter);
        }

        final var samlNameId = SAML2Credentials.SAMLNameID.from(nameId);
        String sessionIndex = null;
        final var sessionIndexes = logoutRequest.getSessionIndexes();
        if (sessionIndexes != null && !sessionIndexes.isEmpty()) {
            final var sessionIndexObject = sessionIndexes.get(0);
            if (sessionIndexObject != null) {
                sessionIndex = sessionIndexObject.getValue();
            }
        }

        final var sloKey = computeSloKey(sessionIndex, samlNameId);
        if (sloKey != null) {
            final var bindingUri = context.getSAMLBindingContext().getBindingUri();
            logger.debug("Using SLO key {} as the session index with the binding uri {}", sloKey, bindingUri);
            if (SAMLConstants.SAML2_SOAP11_BINDING_URI.equals(bindingUri)) {
                logoutHandler.destroySessionBack(context.getWebContext(), context.getSessionStore(), sloKey);
            } else {
                logoutHandler.destroySessionFront(context.getWebContext(), context.getSessionStore(), sloKey);
            }
        }
    }

    /**
     * Validates the SAML logout response.
     *
     * @param logoutResponse the logout response
     * @param context        the context
     * @param engine         the signature engine
     */
    protected void validateLogoutResponse(final LogoutResponse logoutResponse, final SAML2MessageContext context,
                                          final SignatureTrustEngine engine) {

        if (logger.isTraceEnabled()) {
            logger.trace("Validating logout response:\n{}", Configuration.serializeSamlObject(logoutResponse));
        }
        validateSuccess(logoutResponse.getStatus());

        validateSignatureIfItExists(logoutResponse.getSignature(), context, engine);

        validateIssueInstant(logoutResponse.getIssueInstant());

        validateIssuerIfItExists(logoutResponse.getIssuer(), context);

        validateDestinationEndpoint(logoutResponse, context);
    }

    protected void validateDestinationEndpoint(final LogoutResponse logoutResponse, final SAML2MessageContext context) {
        final List expected = new ArrayList<>();
        if (CommonHelper.isBlank(this.expectedDestination)) {
            final Endpoint endpoint = Objects.requireNonNull(context.getSPSSODescriptor().getSingleLogoutServices().get(0));
            if (endpoint.getLocation() != null) {
                expected.add(endpoint.getLocation());
            }
            if (endpoint.getResponseLocation() != null) {
                expected.add(endpoint.getResponseLocation());
            }
        } else {
            expected.add(this.expectedDestination);
        }

        final boolean isDestinationMandatory = context.getSAML2Configuration().isResponseDestinationAttributeMandatory();
        verifyEndpoint(expected, logoutResponse.getDestination(), isDestinationMandatory);
    }

    @Override
    protected void validateSuccess(Status status) {

        if (isPartialLogoutTreatedAsSuccess && status != null && status.getStatusCode() != null) {

            if (StatusCode.PARTIAL_LOGOUT.equals(status.getStatusCode().getValue())) {
                logger.debug(
                    "Response status code is {} and partial logouts are configured to be treated as success => validation successful!",
                    StatusCode.PARTIAL_LOGOUT);
                return;
            }

            logger.debug("Response status code: {}", status.getStatusCode().getValue());

            if (StatusCode.RESPONDER.equals(status.getStatusCode().getValue())
                && status.getStatusCode().getOrderedChildren().stream().filter(StatusCode.class::isInstance).map(StatusCode.class::cast)
                .anyMatch(code -> StatusCode.PARTIAL_LOGOUT.equals(code.getValue()))) {
                logger.debug(
                    "Response sub-status code is {} and partial logouts are configured to be treated as success => validation successful!",
                    StatusCode.PARTIAL_LOGOUT);
                return;
            }
        }

        super.validateSuccess(status);
    }

    public void setActionOnSuccess(final boolean actionOnSuccess) {
        this.actionOnSuccess = actionOnSuccess;
    }

    public void setPostLogoutURL(final String postLogoutURL) {
        this.postLogoutURL = postLogoutURL;
    }

    public void setExpectedDestination(final String expectedDestination) {
        this.expectedDestination = expectedDestination;
    }

    public void setIsPartialLogoutTreatedAsSuccess(final boolean isPartialLogoutTreatedAsSuccess) {
        this.isPartialLogoutTreatedAsSuccess = isPartialLogoutTreatedAsSuccess;
    }

    public String getPostLogoutURL() {
        return postLogoutURL;
    }

    public boolean isActionOnSuccess() {
        return actionOnSuccess;
    }

    public String getExpectedDestination() {
        return expectedDestination;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy