-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: modification réponse SAML assertions encryptées pour conformité…
… à la spec - Suppression du noeud xenc11:MGF dans la balise EncryptionMethod car cela bloque certains clients au moment de la validation du schéma (voir https://www.w3.org/TR/xmlenc-core1/#sec-Alg-KeyTransport)
- Loading branch information
1 parent
dded885
commit dabb1a0
Showing
6 changed files
with
223 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
...ava/org/apereo/cas/web/idp/profile/builders/response/SamlProfileSaml2ResponseBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
package org.apereo.cas.support.saml.web.idp.profile.builders.response; | ||
|
||
import org.apereo.cas.support.saml.SamlIdPConstants; | ||
import org.apereo.cas.support.saml.SamlIdPUtils; | ||
import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPSamlRegisteredServiceCriterion; | ||
import org.apereo.cas.support.saml.services.idp.metadata.MetadataEntityAttributeQuery; | ||
import org.apereo.cas.support.saml.web.idp.profile.builders.SamlProfileBuilderContext; | ||
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.encoder.sso.SamlResponseArtifactEncoder; | ||
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.encoder.sso.SamlResponsePostEncoder; | ||
import org.apereo.cas.support.saml.web.idp.profile.builders.enc.encoder.sso.SamlResponsePostSimpleSignEncoder; | ||
import org.apereo.cas.ticket.query.SamlAttributeQueryTicket; | ||
import org.apereo.cas.ticket.query.SamlAttributeQueryTicketFactory; | ||
import org.apereo.cas.util.RandomUtils; | ||
import org.apereo.cas.util.function.FunctionUtils; | ||
import org.apereo.cas.web.support.CookieUtils; | ||
import lombok.extern.slf4j.Slf4j; | ||
import lombok.val; | ||
import net.shibboleth.shared.resolver.CriteriaSet; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
import org.jooq.lambda.Unchecked; | ||
import org.opensaml.saml.common.SAMLVersion; | ||
import org.opensaml.saml.common.xml.SAMLConstants; | ||
import org.opensaml.saml.metadata.criteria.entity.impl.EvaluableEntityRoleEntityDescriptorCriterion; | ||
import org.opensaml.saml.saml2.core.Assertion; | ||
import org.opensaml.saml.saml2.core.Attribute; | ||
import org.opensaml.saml.saml2.core.AttributeQuery; | ||
import org.opensaml.saml.saml2.core.AuthnRequest; | ||
import org.opensaml.saml.saml2.core.EncryptedAssertion; | ||
import org.opensaml.saml.saml2.core.NameID; | ||
import org.opensaml.saml.saml2.core.Response; | ||
import org.opensaml.saml.saml2.core.Status; | ||
import org.opensaml.saml.saml2.core.StatusCode; | ||
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; | ||
import org.springframework.core.annotation.AnnotationAwareOrderComparator; | ||
|
||
import java.io.Serial; | ||
import java.time.ZoneOffset; | ||
import java.time.ZonedDateTime; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
|
||
/** | ||
* This is {@link SamlProfileSaml2ResponseBuilder}. | ||
* | ||
* @author Misagh Moayyed | ||
* @since 5.1.0 | ||
*/ | ||
@Slf4j | ||
public class SamlProfileSaml2ResponseBuilder extends BaseSamlProfileSamlResponseBuilder<Response> { | ||
@Serial | ||
private static final long serialVersionUID = 1488837627964481272L; | ||
|
||
public SamlProfileSaml2ResponseBuilder(final SamlProfileSamlResponseBuilderConfigurationContext configurationContext) { | ||
super(configurationContext); | ||
} | ||
|
||
@Override | ||
public Response buildResponse(final Optional<Assertion> assertion, | ||
final SamlProfileBuilderContext context) throws Exception { | ||
val id = '_' + String.valueOf(RandomUtils.nextLong()); | ||
|
||
val entityId = getConfigurationContext().getCasProperties().getAuthn().getSamlIdp().getCore().getEntityId(); | ||
val recipient = getInResponseTo(context.getSamlRequest(), entityId, context.getRegisteredService().isSkipGeneratingResponseInResponseTo()); | ||
val samlResponse = newResponse(id, ZonedDateTime.now(ZoneOffset.UTC), recipient, null); | ||
samlResponse.setVersion(SAMLVersion.VERSION_20); | ||
|
||
val issuerId = FunctionUtils.doIf(StringUtils.isNotBlank(context.getRegisteredService().getIssuerEntityId()), | ||
context.getRegisteredService()::getIssuerEntityId, | ||
Unchecked.supplier(() -> { | ||
val criteriaSet = new CriteriaSet( | ||
new EvaluableEntityRoleEntityDescriptorCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME), | ||
new SamlIdPSamlRegisteredServiceCriterion(context.getRegisteredService())); | ||
LOGGER.trace("Resolving entity id from SAML2 IdP metadata to determine issuer for [{}]", context.getRegisteredService().getName()); | ||
val entityDescriptor = Objects.requireNonNull(getConfigurationContext().getSamlIdPMetadataResolver().resolveSingle(criteriaSet)); | ||
return entityDescriptor.getEntityID(); | ||
})) | ||
.get(); | ||
|
||
samlResponse.setIssuer(buildSamlResponseIssuer(issuerId)); | ||
val acs = SamlIdPUtils.determineEndpointForRequest(Pair.of(context.getSamlRequest(), context.getMessageContext()), | ||
context.getAdaptor(), context.getBinding()); | ||
val location = StringUtils.isBlank(acs.getResponseLocation()) ? acs.getLocation() : acs.getResponseLocation(); | ||
samlResponse.setDestination(location); | ||
|
||
if (getConfigurationContext().getCasProperties() | ||
.getAuthn().getSamlIdp().getCore().isAttributeQueryProfileEnabled()) { | ||
storeAttributeQueryTicketInRegistry(assertion, context); | ||
} | ||
|
||
val finalAssertion = encryptAssertion(assertion, context); | ||
if (finalAssertion.isPresent()) { | ||
val result = finalAssertion.get(); | ||
if (result instanceof final EncryptedAssertion encrypted) { | ||
LOGGER.trace("Built assertion is encrypted, so the response will add it to the encrypted assertions collection"); | ||
// Customization : remove xenc11:MGF node from EncryptionMethod to fix error when validating xml schema | ||
// See https://www.w3.org/TR/xmlenc-core1/#sec-RSA-OAEP as mgf1sha1 is already the default method used | ||
val encryptionMethod = encrypted.getEncryptedKeys().getFirst().getEncryptionMethod(); | ||
if(encryptionMethod.getAlgorithm().equals("http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p")){ | ||
if(encryptionMethod.getDOM().getLastChild().getNodeName().equals("xenc11:MGF")){ | ||
encryptionMethod.getDOM().removeChild(encryptionMethod.getDOM().getLastChild()); | ||
LOGGER.debug("Removed xenc11:MGF node from EncryptionMethod to ensure xml schema validation from all SAML clients"); | ||
} | ||
} | ||
samlResponse.getEncryptedAssertions().add(encrypted); | ||
} else if (result instanceof final Assertion nonEncryptedAssertion) { | ||
LOGGER.trace("Built assertion is not encrypted, so the response will add it to the assertions collection"); | ||
samlResponse.getAssertions().add(nonEncryptedAssertion); | ||
} | ||
} | ||
|
||
samlResponse.setStatus(determineResponseStatus(context)); | ||
|
||
val customizers = configurationContext.getApplicationContext() | ||
.getBeansOfType(SamlIdPResponseCustomizer.class).values(); | ||
customizers.stream() | ||
.sorted(AnnotationAwareOrderComparator.INSTANCE) | ||
.forEach(customizer -> customizer.customizeResponse(context, this, samlResponse)); | ||
|
||
openSamlConfigBean.logObject(samlResponse); | ||
|
||
if (signSamlResponseFor(context)) { | ||
LOGGER.debug("SAML entity id [{}] indicates that SAML responses should be signed", context.getAdaptor().getEntityId()); | ||
val samlResponseSigned = getConfigurationContext().getSamlObjectSigner().encode(samlResponse, | ||
context.getRegisteredService(), context.getAdaptor(), context.getHttpResponse(), context.getHttpRequest(), | ||
context.getBinding(), context.getSamlRequest(), context.getMessageContext()); | ||
openSamlConfigBean.logObject(samlResponseSigned); | ||
return samlResponseSigned; | ||
} | ||
|
||
return samlResponse; | ||
} | ||
|
||
protected boolean signSamlResponseFor(final SamlProfileBuilderContext context) { | ||
return context.getRegisteredService().getSignResponses().isTrue() | ||
|| SamlIdPUtils.doesEntityDescriptorMatchEntityAttribute(context.getAdaptor().getEntityDescriptor(), | ||
List.of(MetadataEntityAttributeQuery.of(SamlIdPConstants.KnownEntityAttributes.SHIBBOLETH_SIGN_RESPONSES.getName(), | ||
Attribute.URI_REFERENCE, List.of(Boolean.TRUE.toString())))); | ||
} | ||
|
||
protected Status determineResponseStatus(final SamlProfileBuilderContext context) { | ||
if (context.getAuthenticatedAssertion().isEmpty()) { | ||
if (context.getSamlRequest() instanceof final AuthnRequest authnRequest && authnRequest.isPassive()) { | ||
val message = """ | ||
SAML2 authentication request from %s indicated a passive authentication request, \ | ||
but CAS is unable to satisfy and support this requirement, likely because \ | ||
no existing single sign-on session is available yet to build the SAML2 response. | ||
""".formatted(context.getAdaptor().getEntityId()).stripIndent().trim(); | ||
return newStatus(StatusCode.NO_PASSIVE, message); | ||
} | ||
return newStatus(StatusCode.AUTHN_FAILED, null); | ||
} | ||
return newStatus(StatusCode.SUCCESS, null); | ||
} | ||
|
||
@Override | ||
protected Response encode(final SamlProfileBuilderContext context, | ||
final Response samlResponse, | ||
final String relayState) throws Exception { | ||
LOGGER.trace("Constructing encoder based on binding [{}] for [{}]", context.getBinding(), context.getAdaptor().getEntityId()); | ||
if (context.getBinding().equalsIgnoreCase(SAMLConstants.SAML2_ARTIFACT_BINDING_URI)) { | ||
val encoder = new SamlResponseArtifactEncoder( | ||
getConfigurationContext().getVelocityEngineFactory(), | ||
context.getAdaptor(), context.getHttpRequest(), context.getHttpResponse(), | ||
getConfigurationContext().getSamlArtifactMap()); | ||
return encoder.encode(context.getSamlRequest(), samlResponse, relayState, context.getMessageContext()); | ||
} | ||
|
||
if (context.getBinding().equalsIgnoreCase(SAMLConstants.SAML2_POST_SIMPLE_SIGN_BINDING_URI)) { | ||
val encoder = new SamlResponsePostSimpleSignEncoder(getConfigurationContext().getVelocityEngineFactory(), | ||
context.getAdaptor(), context.getHttpResponse(), context.getHttpRequest()); | ||
return encoder.encode(context.getSamlRequest(), samlResponse, relayState, context.getMessageContext()); | ||
} | ||
|
||
val encoder = new SamlResponsePostEncoder(getConfigurationContext().getVelocityEngineFactory(), context.getAdaptor(), context.getHttpResponse(), context.getHttpRequest()); | ||
return encoder.encode(context.getSamlRequest(), samlResponse, relayState, context.getMessageContext()); | ||
} | ||
|
||
private void storeAttributeQueryTicketInRegistry(final Optional<Assertion> assertion, final SamlProfileBuilderContext context) | ||
throws Exception { | ||
val existingQuery = context.getHttpRequest().getAttribute(AttributeQuery.class.getSimpleName()); | ||
if (existingQuery == null && assertion.isPresent()) { | ||
val nameId = (String) context.getHttpRequest().getAttribute(NameID.class.getName()); | ||
val ticketGrantingTicket = CookieUtils.getTicketGrantingTicketFromRequest( | ||
getConfigurationContext().getTicketGrantingTicketCookieGenerator(), | ||
getConfigurationContext().getTicketRegistry(), context.getHttpRequest()); | ||
|
||
if (ticketGrantingTicket != null) { | ||
val samlAttributeQueryTicketFactory = (SamlAttributeQueryTicketFactory) getConfigurationContext().getTicketFactory().get(SamlAttributeQueryTicket.class); | ||
val ticket = samlAttributeQueryTicketFactory.create(nameId, assertion.get(), context.getAdaptor().getEntityId(), ticketGrantingTicket); | ||
getConfigurationContext().getTicketRegistry().addTicket(ticket); | ||
context.getHttpRequest().setAttribute(SamlAttributeQueryTicket.class.getName(), ticket); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters