se.swedenconnect.spring.saml.idp.response.Saml2ResponseBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-saml-idp Show documentation
Show all versions of spring-saml-idp Show documentation
Spring SAML Identity Provider Core
/*
* Copyright 2023-2024 Sweden Connect
*
* 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 se.swedenconnect.spring.saml.idp.response;
import java.time.Instant;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.Issuer;
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.EntityDescriptor;
import org.opensaml.xmlsec.SecurityConfigurationSupport;
import org.opensaml.xmlsec.encryption.EncryptedData;
import org.opensaml.xmlsec.encryption.support.EncryptionException;
import org.opensaml.xmlsec.signature.support.SignatureException;
import org.springframework.context.MessageSource;
import org.springframework.security.config.Customizer;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import net.shibboleth.shared.component.ComponentInitializationException;
import se.swedenconnect.opensaml.xmlsec.encryption.support.SAMLObjectEncrypter;
import se.swedenconnect.opensaml.xmlsec.signature.support.SAMLObjectSigner;
import se.swedenconnect.security.credential.PkiCredential;
import se.swedenconnect.security.credential.opensaml.OpenSamlCredential;
import se.swedenconnect.spring.saml.idp.error.Saml2ErrorStatusException;
import se.swedenconnect.spring.saml.idp.error.UnrecoverableSaml2IdpError;
import se.swedenconnect.spring.saml.idp.error.UnrecoverableSaml2IdpException;
import se.swedenconnect.spring.saml.idp.events.Saml2IdpEventPublisher;
import se.swedenconnect.spring.saml.idp.utils.DefaultSaml2MessageIDGenerator;
import se.swedenconnect.spring.saml.idp.utils.Saml2MessageIDGenerator;
/**
* Builds a SAML {@link Response} message.
*
* @author Martin Lindström
*/
@Slf4j
public class Saml2ResponseBuilder {
/** Event publisher. */
private final Saml2IdpEventPublisher eventPublisher;
/** The issuer entityID for the {@link Response} objects being created. */
private final String responseIssuer;
/** The IdP signing credential. */
private final OpenSamlCredential signingCredential;
/** Whether assertions should be encrypted. */
private boolean encryptAssertions = false;
/** For encrypting assertions. */
private SAMLObjectEncrypter samlEncrypter;
/** For customizing the {@link Response}. */
private Customizer responseCustomizer = Customizer.withDefaults();
/** The ID generator - defaults to {@link DefaultSaml2MessageIDGenerator}. */
private Saml2MessageIDGenerator idGenerator = new DefaultSaml2MessageIDGenerator();
/** Optional message source for resolving error messages. */
private MessageSource messageSource;
/**
* Constructor.
*
* @param idpEntityId the entityID for the IdP
* @param signingCredential the IdP signing credential (for signing of {@link Response} objects)
* @param eventPublisher the event publisher
*/
public Saml2ResponseBuilder(final String idpEntityId, final PkiCredential signingCredential,
final Saml2IdpEventPublisher eventPublisher) {
this.responseIssuer = Optional.ofNullable(idpEntityId).filter(StringUtils::hasText)
.orElseThrow(() -> new IllegalArgumentException("idpEntityId must be set"));
Assert.notNull(signingCredential, "signingCredential must not be null");
this.signingCredential = new OpenSamlCredential(signingCredential);
this.eventPublisher = Objects.requireNonNull(eventPublisher, "eventPublisher must not be null");
}
/**
* Given an error {@link Status} object, the method builds a {@link Response} object indicating the error and signs
* it.
*
* @param responseAttributes the response attributes needed for building the {@link Response} object
* @param errorStatus the SAML status object
* @return a {@link Response} object
* @throws UnrecoverableSaml2IdpException for errors
*/
public Response buildErrorResponse(final Saml2ResponseAttributes responseAttributes, final Status errorStatus) {
Assert.notNull(errorStatus, "errorStatus must not be null");
final String code = Optional.ofNullable(errorStatus.getStatusCode())
.map(StatusCode::getValue)
.orElseThrow(() -> new IllegalArgumentException("Supplied status object does not have status code set"));
if (StatusCode.SUCCESS.equals(code)) {
throw new IllegalArgumentException("Can not send error response with status set to success");
}
final Response response = this.createResponse(responseAttributes, errorStatus);
this.responseCustomizer.customize(response);
this.signResponse(response, responseAttributes.getPeerMetadata());
this.eventPublisher.publishSamlErrorResponse(response, responseAttributes.getPeerMetadata().getEntityID());
return response;
}
/**
* Given a {@link Saml2ErrorStatusException} exception, the method builds a {@link Response} object indicating the
* error {@link Status} given by the exception and signs it.
*
* @param responseAttributes the response attributes needed for building the {@link Response} object
* @param error the SAML error
* @return a {@link Response} object
* @throws UnrecoverableSaml2IdpException for errors
*/
public Response buildErrorResponse(final Saml2ResponseAttributes responseAttributes,
final Saml2ErrorStatusException error) throws UnrecoverableSaml2IdpException {
final Status status = this.messageSource != null
? error.getStatus(this.messageSource, Locale.ENGLISH)
: error.getStatus();
return this.buildErrorResponse(responseAttributes, status);
}
/**
* Given an {@link Assertion}, the method builds a {@link Response} object including the supplied {@link Assertion}.
* If the Identity Provider is configured to encrypt assertions, the method encrypts the supplied {@link Assertion}
* for the recipient given by {@link Saml2ResponseAttributes#getPeerMetadata()}.
*
* @param responseAttributes the response attributes needed for building the {@link Response} object
* @param assertion the SAML {@link Assertion}
* @return a {@link Response} object
* @throws UnrecoverableSaml2IdpException for errors
*/
public Response buildResponse(final Saml2ResponseAttributes responseAttributes, final Assertion assertion)
throws UnrecoverableSaml2IdpException {
final Status status = (Status) XMLObjectSupport.buildXMLObject(Status.DEFAULT_ELEMENT_NAME);
final StatusCode sc = (StatusCode) XMLObjectSupport.buildXMLObject(StatusCode.DEFAULT_ELEMENT_NAME);
sc.setValue(StatusCode.SUCCESS);
status.setStatusCode(sc);
final Response response = this.createResponse(responseAttributes, status);
if (this.isEncryptAssertions()) {
final EncryptedAssertion encryptedAssertion =
this.encryptAssertion(assertion, responseAttributes.getPeerMetadata());
response.getEncryptedAssertions().add(encryptedAssertion);
}
else {
response.getAssertions().add(assertion);
}
this.responseCustomizer.customize(response);
this.signResponse(response, responseAttributes.getPeerMetadata());
this.eventPublisher.publishSamlSuccessResponse(
response, assertion, responseAttributes.getPeerMetadata().getEntityID());
return response;
}
/**
* Creates a {@link Response} object with the basic attributes {@code ID}, {@code Destination} and
* {@code InResponseTo} as well as the {@code Issuer} element and the supplied {@code Status} element.
*
* @param responseAttributes the response attributes needed for building the {@link Response} object
* @param status the SAML {@link Status} object
* @return a {@link Response} object
* @throws UnrecoverableSaml2IdpException for errors
*/
protected Response createResponse(final Saml2ResponseAttributes responseAttributes, final Status status)
throws UnrecoverableSaml2IdpException {
if (responseAttributes.getDestination() == null || responseAttributes.getInResponseTo() == null || status == null) {
throw new UnrecoverableSaml2IdpException(UnrecoverableSaml2IdpError.INTERNAL, "No response data available", null);
}
final Response samlResponse = (Response) XMLObjectSupport.buildXMLObject(Response.DEFAULT_ELEMENT_NAME);
samlResponse.setStatus(status);
samlResponse.setID(this.idGenerator.generateIdentifier());
samlResponse.setDestination(responseAttributes.getDestination());
samlResponse.setInResponseTo(responseAttributes.getInResponseTo());
final Issuer issuer = (Issuer) XMLObjectSupport.buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue(this.responseIssuer);
samlResponse.setIssuer(issuer);
samlResponse.setIssueInstant(Instant.now());
return samlResponse;
}
/**
* Signs the {@link Response} message.
*
* @param samlResponse the object to sign
* @param peerMetadata the peer metadata (may be used to select signing algorithm)
* @throws UnrecoverableSaml2IdpException for signing errors
*/
protected void signResponse(final Response samlResponse, final EntityDescriptor peerMetadata)
throws UnrecoverableSaml2IdpException {
try {
SAMLObjectSigner.sign(samlResponse, this.signingCredential,
SecurityConfigurationSupport.getGlobalSignatureSigningConfiguration(), peerMetadata);
log.debug("Response message successfully signed [destination: '{}', id: '{}', in-response-to: {}]",
samlResponse.getDestination(), samlResponse.getID(), samlResponse.getInResponseTo());
}
catch (final SignatureException e) {
log.error("Failed to sign Response message - {} [destination: '{}', id: '{}', in-response-to: {}]",
e.getMessage(), samlResponse.getDestination(), samlResponse.getID(), samlResponse.getInResponseTo(), e);
throw new UnrecoverableSaml2IdpException(UnrecoverableSaml2IdpError.INTERNAL,
"Failed to sign Response message", e,
new UnrecoverableSaml2IdpException.TraceAuthentication(
samlResponse.getInResponseTo(), peerMetadata.getEntityID()));
}
}
/**
* Encrypts the supplied {@link Assertion}.
*
* @param assertion the assertion to encrypt
* @param peerMetadata the metadata for the peer to whom we encrypt for
* @return an {@link EncryptedAssertion}
* @throws UnrecoverableSaml2IdpException for unrecoverable errors
*/
protected EncryptedAssertion encryptAssertion(final Assertion assertion, final EntityDescriptor peerMetadata)
throws UnrecoverableSaml2IdpException {
if (peerMetadata == null) {
throw new UnrecoverableSaml2IdpException(UnrecoverableSaml2IdpError.INTERNAL, "No response data available", null);
}
try {
final EncryptedAssertion encryptedAssertion =
(EncryptedAssertion) XMLObjectSupport.buildXMLObject(EncryptedAssertion.DEFAULT_ELEMENT_NAME);
final EncryptedData encryptedData =
this.samlEncrypter.encrypt(assertion, new SAMLObjectEncrypter.Peer(peerMetadata));
encryptedAssertion.setEncryptedData(encryptedData);
return encryptedAssertion;
}
catch (final EncryptionException e) {
throw new UnrecoverableSaml2IdpException(UnrecoverableSaml2IdpError.INTERNAL, "Failed to encrypt assertion", e,
new UnrecoverableSaml2IdpException.TraceAuthentication(null, peerMetadata.getEntityID()));
}
}
/**
* Tells whether assertions are encrypted.
*
* @return {@code true} if assertions are encrypted, and {@code false} otherwise
*/
public boolean isEncryptAssertions() {
return this.encryptAssertions;
}
/**
* Assigns whether assertions should be encrypted.
*
* @param encryptAssertions whether assertions should be encrypted
*/
public void setEncryptAssertions(final boolean encryptAssertions) {
this.encryptAssertions = encryptAssertions;
if (this.encryptAssertions) {
try {
this.samlEncrypter = new SAMLObjectEncrypter();
}
catch (final ComponentInitializationException e) {
throw new SecurityException("Failed to initialize encrypter", e);
}
}
else {
this.samlEncrypter = null;
}
}
/**
* Assigns a custom ID generator. The default is {@link DefaultSaml2MessageIDGenerator}.
*
* @param idGenerator the ID generator
*/
public void setIdGenerator(final Saml2MessageIDGenerator idGenerator) {
this.idGenerator = idGenerator;
}
/**
* By assigning a {@link Customizer} the {@link Response} object that is built can be modified. The customizer is
* invoked when the {@link Response} object has been completely built, but before it is signed.
*
* @param responseCustomizer a {@link Customizer}
*/
public void setResponseCustomizer(final Customizer responseCustomizer) {
this.responseCustomizer = Objects.requireNonNull(responseCustomizer, "responseCustomizer must not be null");
}
/**
* Assigns a message source for resolving error messages.
*
* @param messageSource the {@link MessageSource}
*/
public void setMessageSource(final MessageSource messageSource) {
this.messageSource = messageSource;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy