apereo/cas · error · SAMLException

Logout request is not signed but should be for service

Error message

Logout request is not signed but should be for service %s

What it means

The SAML IdP single-logout handler enforces that incoming LogoutRequests are signed when the registered service requires it (service's signLogoutRequest or the global forceSignedLogoutRequests setting). When SAMLBindingSupport.isMessageSigned reports the message is unsigned, the handler rejects it with this SAMLException. This is a policy enforcement on the SP's logout binding.

Solutions

  1. Configure the SP to sign its LogoutRequests (set its AuthnRequestsSigned/SLO signing on)
  2. Or set signLogoutRequest=false for that SamlRegisteredService in the CAS service registry
  3. Or disable cas.authn.samlIdp.logout.force-signed-logout-requests if policy permits
  4. Verify the SP uses POST binding with signature, or signs redirect-binding messages via SigAlg/Signature query params

Example fix

// before (CAS service registry JSON)
"@class": "...SamlRegisteredService", "signLogoutRequest": "TRUE"
// after: allow unsigned logout for this SP
"@class": "...SamlRegisteredService", "signLogoutRequest": "FALSE"
Defensive patterns

Strategy: validation

Validate before calling

// SP side: confirm the logout message is signed before dispatching to the IdP SLO endpoint
if (!logoutMessage.isSigned() && idpRequiresSignedLogout) {
    Signer.signObject(logoutMessage); // or configure the SP signing key for SLO bindings
}

Prevention

When it happens

Trigger: handleLogoutRequest -> ensureLogoutRequestIsSignedIfNecessary when registeredService.getSignLogoutRequest() is true (or samlIdp logout.forceSignedLogoutRequests=true and service is undefined) and the received LogoutRequest arrived over a binding with no signature (e.g. unsigned redirect binding).

Common situations: SP sends unsigned SLO redirect messages while CAS service config mandates signing; operator enabled forceSignedLogoutRequests globally but a legacy SP cannot sign; signLogoutRequest flipped to true in service registry without updating the SP.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/c34e2ab0d64bf7d7. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-saml-idp-web/src/main/java/org/apereo/cas/support/saml/web/idp/profile/slo/AbstractSamlSLOProfileHandlerController.java:135

            .filter(processor -> processor.supports(request, response, logoutRequest, messageContext))
            .forEach(Unchecked.consumer(processor -> processor.receive(request, response, logoutRequest, messageContext)));

        val requestDispatcher = request.getServletContext().getRequestDispatcher(CasProtocolConstants.ENDPOINT_LOGOUT);
        requestDispatcher.forward(request, response);
    }

    protected void ensureLogoutRequestIsSignedIfNecessary(final SamlRegisteredService registeredService,
                                                          final MessageContext messageContext) throws SAMLException {
        var ensureSignature = false;
        if (registeredService.getSignLogoutRequest().isUndefined()) {
            val logout = configurationContext.getCasProperties().getAuthn().getSamlIdp().getLogout();
            ensureSignature = logout.isForceSignedLogoutRequests();
        } else {
            ensureSignature = registeredService.getSignLogoutRequest().isTrue();
        }

        if (ensureSignature && !SAMLBindingSupport.isMessageSigned(messageContext)) {
            throw new SAMLException("Logout request is not signed but should be for service %s"
                .formatted(registeredService.getServiceId()));
        }
    }

    protected <T> T buildSamlObject(final QName qname, final Class<T> clazz) {
        val builderFactory = getConfigurationContext().getOpenSamlConfigBean().getBuilderFactory();
        val builder = (SAMLObjectBuilder) builderFactory.getBuilder(qname);
        return clazz.cast(Objects.requireNonNull(builder).buildObject());
    }

    protected void handleSloProfileRequest(final HttpServletResponse response,
                                           final HttpServletRequest request,
                                           final BaseHttpServletRequestXMLMessageDecoder decoder,
                                           final String logoutRequestBinding) throws Throwable {
        val logout = getConfigurationContext().getCasProperties().getAuthn().getSamlIdp().getLogout();
        if (logout.isSingleLogoutCallbacksDisabled()) {
            LOGGER.info("Processing SAML2 IdP SLO requests is disabled");
            return;

View on GitHub (pinned to e7288fc434)