Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ allprojects {
mavenCentral()
maven {
url = uri("https://build.shibboleth.net/nexus/content/repositories/releases/")
content {
includeGroup("org.opensaml")
includeGroup("net.shibboleth")
includeGroupByRegex("net\\.shibboleth\\..*")
}
}
maven { url = uri("https://repository.mulesoft.org/releases/") }
}
Expand Down Expand Up @@ -79,7 +84,7 @@ subprojects {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'org.opensaml' && details.requested.name.startsWith("opensaml-")) {
details.useVersion "${versions.opensaml}"
details.because 'Spring Security 5.8.x allows OpenSAML 3 or 4. OpenSAML 3 has reached its end-of-life. Spring Security 6 drops support for 3, using 4.'
details.because 'Pinning all opensaml modules to the same version for OpenSAML 5 migration.'
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ versions.springBootVersion = "3.5.13"
versions.guavaVersion = "33.5.0-jre"
versions.seleniumVersion = "4.43.0"
versions.braveVersion = "6.3.1"
versions.opensaml = "4.3.2"
versions.opensaml = "5.1.6"

// Versions we're overriding from the Spring Boot Bom (Dependabot does not issue PRs to bump these versions, so we need to manually bump them)
ext["selenium.version"] = "${versions.seleniumVersion}" // Selenium for integration tests only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import jakarta.annotation.Nonnull;
import lombok.Getter;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import net.shibboleth.shared.xml.ParserPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
Expand All @@ -34,6 +34,7 @@
import org.opensaml.core.xml.schema.XSInteger;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.core.xml.schema.XSURI;
import org.opensaml.saml.common.assertion.AssertionValidationException;
import org.opensaml.saml.common.assertion.ValidationContext;
import org.opensaml.saml.common.assertion.ValidationResult;
import org.opensaml.saml.saml2.assertion.ConditionValidator;
Expand Down Expand Up @@ -103,13 +104,17 @@
import static org.cloudfoundry.identity.uaa.util.UaaUrlUtils.normalizeUrlForPortComparison;

/**
* This was copied from Spring Security, and modified to work with Open SAML 4.0.x
* The original class only works with Open SAML 4.1.x+
* This was originally copied from Spring Security, and modified to work with Open SAML 4.0.x,
* then further updated for Open SAML 5.x compatibility.
* <p/>
* Once we can move to the spring-security version of OpenSaml4AuthenticationProvider,
* this class should be removed, along with OpenSamlDecryptionUtils and OpenSamlVerificationUtils.
* Key changes from OpenSAML 4 to 5:
* - {@code net.shibboleth.utilities.java.support} packages moved to {@code net.shibboleth.shared}
* - {@code SAML20AssertionValidator} constructor gains a 4th {@code AssertionValidator} parameter
* - {@code ValidationContext.getValidationFailureMessage()} renamed to {@code getValidationFailureMessages()}
* - {@code ConditionValidator.validate()} now throws {@code AssertionValidationException}
* - {@code BearerSubjectConfirmationValidator.validateAddress()} removed (address validation dropped)
*/
public final class OpenSaml4AuthenticationProvider implements AuthenticationProvider, ZoneAware {
public final class OpenSaml5AuthenticationProvider implements AuthenticationProvider, ZoneAware {

static {
SamlConfiguration.setupOpenSaml();
Expand Down Expand Up @@ -144,9 +149,9 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
private Converter<ResponseToken, ? extends AbstractAuthenticationToken> responseAuthenticationConverter = createDefaultResponseAuthenticationConverter();

/**
* Creates an {@link OpenSaml4AuthenticationProvider}
* Creates an {@link OpenSaml5AuthenticationProvider}
*/
public OpenSaml4AuthenticationProvider() {
public OpenSaml5AuthenticationProvider() {
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.responseUnmarshaller = (ResponseUnmarshaller) registry.getUnmarshallerFactory()
.getUnmarshaller(Response.DEFAULT_ELEMENT_NAME);
Expand All @@ -160,7 +165,7 @@ public OpenSaml4AuthenticationProvider() {
* {@link #createDefaultResponseValidator()}, like so:
*
* <pre>
* OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
* OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
* provider.setResponseValidator(responseToken -&gt; {
* Saml2ResponseValidatorResult result = createDefaultResponseValidator()
* .convert(responseToken)
Expand Down Expand Up @@ -574,7 +579,7 @@ private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAss
}
String message = "Invalid assertion [%s] for SAML response [%s]: %s".formatted(assertion.getID(),
assertion.getParent() != null ? ((Response) assertion.getParent()).getID() : assertion.getID(),
context.getValidationFailureMessage());
String.join("; ", context.getValidationFailureMessages()));
return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
};
}
Expand All @@ -601,6 +606,8 @@ private static ValidationContext createValidationContext(AssertionToken assertio
params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
params.put(SAML2AssertionValidationParameters.VALID_ISSUERS, Collections.singleton(assertingPartyEntityId));
// Disable address checking - we don't track valid client addresses
params.put(SAML2AssertionValidationParameters.SC_CHECK_ADDRESS, false);
paramsConsumer.accept(params);
return new ValidationContext(params);
}
Expand Down Expand Up @@ -675,54 +682,53 @@ public QName getServicedCondition() {

@Nonnull
@Override
public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context) {
public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context)
throws AssertionValidationException {
// applications should validate their own OneTimeUse conditions
return ValidationResult.VALID;
}
});
conditions.add(new ProxyRestrictionConditionValidator());
subjects.add(new BearerSubjectConfirmationValidator() {
@Override
protected ValidationResult validateAddress(SubjectConfirmation confirmation, Assertion assertion,
ValidationContext context, boolean required) {
// applications should validate their own addresses - gh-7514
return ValidationResult.VALID;
}
});
subjects.add(new BearerSubjectConfirmationValidator());
}

private static final SAML20AssertionValidator attributeValidator = new SAML20AssertionValidator(conditions,
subjects, statements, null, null) {
subjects, statements, null, null, null) {
@Nonnull
@Override
protected ValidationResult validateSignature(Assertion token, ValidationContext context) {
protected ValidationResult validateSignature(Assertion token, ValidationContext context)
throws AssertionValidationException {
return ValidationResult.VALID;
}
};

static SAML20AssertionValidator createSignatureValidator(SignatureTrustEngine engine) {
return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), engine,
return new SAML20AssertionValidator(new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), null, engine,
validator) {
@Nonnull
@Override
protected ValidationResult validateConditions(Assertion assertion, ValidationContext context) {
protected ValidationResult validateConditions(Assertion assertion, ValidationContext context)
throws AssertionValidationException {
return ValidationResult.VALID;
}

@Nonnull
@Override
protected ValidationResult validateSubjectConfirmation(Assertion assertion, ValidationContext context) {
protected ValidationResult validateSubjectConfirmation(Assertion assertion, ValidationContext context)
throws AssertionValidationException {
return ValidationResult.VALID;
}

@Nonnull
@Override
protected ValidationResult validateStatements(Assertion assertion, ValidationContext context) {
protected ValidationResult validateStatements(Assertion assertion, ValidationContext context)
throws AssertionValidationException {
return ValidationResult.VALID;
}

@Override
protected ValidationResult validateIssuer(Assertion assertion, ValidationContext context) {
protected ValidationResult validateIssuer(Assertion assertion, ValidationContext context)
throws AssertionValidationException {
return ValidationResult.VALID;
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package org.cloudfoundry.identity.uaa.provider.saml;

import net.shibboleth.shared.xml.SerializeSupport;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Marshaller;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.signature.KeyInfo;
import org.opensaml.xmlsec.signature.X509Certificate;
import org.opensaml.xmlsec.signature.X509Data;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
import org.springframework.security.saml2.provider.service.metadata.Saml2MetadataResolver;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.util.Assert;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;

/**
* OpenSAML 5 compatible replacement for Spring Security's {@link OpenSamlMetadataResolver}.
* <p>
* Spring Security's {@code OpenSamlMetadataResolver} uses the OpenSAML 4 internal class
* {@code net.shibboleth.utilities.java.support.xml.SerializeSupport} which is not present
* when using OpenSAML 5. This implementation uses the OpenSAML 5 equivalent
* {@code net.shibboleth.shared.xml.SerializeSupport} directly.
*/
public final class OpenSaml5MetadataResolver implements Saml2MetadataResolver {

private Consumer<OpenSamlMetadataResolver.EntityDescriptorParameters> entityDescriptorCustomizer = (parameters) -> {
};

public OpenSaml5MetadataResolver() {
}

@Override
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
EntityDescriptor entityDescriptor = buildEntityDescriptor(relyingPartyRegistration);
return serialize(entityDescriptor);
}

/**
* Set a {@link Consumer} for modifying the OpenSAML {@link EntityDescriptor}.
*/
public void setEntityDescriptorCustomizer(Consumer<OpenSamlMetadataResolver.EntityDescriptorParameters> entityDescriptorCustomizer) {
Assert.notNull(entityDescriptorCustomizer, "entityDescriptorCustomizer cannot be null");
this.entityDescriptorCustomizer = entityDescriptorCustomizer;
}

private EntityDescriptor buildEntityDescriptor(RelyingPartyRegistration registration) {
EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
entityDescriptor.setEntityID(registration.getEntityId());
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(registration);
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
this.entityDescriptorCustomizer.accept(
new OpenSamlMetadataResolver.EntityDescriptorParameters(entityDescriptor, registration));
return entityDescriptor;
}

private SPSSODescriptor buildSpSsoDescriptor(RelyingPartyRegistration registration) {
SPSSODescriptor spSsoDescriptor = build(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
spSsoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getSigningX509Credentials(), UsageType.SIGNING));
spSsoDescriptor.getKeyDescriptors()
.addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION));
spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration));
if (registration.getSingleLogoutServiceLocation() != null) {
for (Saml2MessageBinding binding : registration.getSingleLogoutServiceBindings()) {
spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration, binding));
}
}
if (registration.getNameIdFormat() != null) {
spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration));
}
return spSsoDescriptor;
}

private List<KeyDescriptor> buildKeys(Collection<Saml2X509Credential> credentials, UsageType usageType) {
List<KeyDescriptor> list = new ArrayList<>();
for (Saml2X509Credential credential : credentials) {
list.add(buildKeyDescriptor(usageType, credential.getCertificate()));
}
return list;
}

private KeyDescriptor buildKeyDescriptor(UsageType usageType, java.security.cert.X509Certificate certificate) {
KeyDescriptor keyDescriptor = build(KeyDescriptor.DEFAULT_ELEMENT_NAME);
KeyInfo keyInfo = build(KeyInfo.DEFAULT_ELEMENT_NAME);
X509Certificate x509Certificate = build(X509Certificate.DEFAULT_ELEMENT_NAME);
X509Data x509Data = build(X509Data.DEFAULT_ELEMENT_NAME);
try {
x509Certificate.setValue(new String(Base64.getEncoder().encode(certificate.getEncoded())));
} catch (CertificateEncodingException ex) {
throw new Saml2Exception("Cannot encode certificate " + certificate);
}
x509Data.getX509Certificates().add(x509Certificate);
keyInfo.getX509Datas().add(x509Data);
keyDescriptor.setUse(usageType);
keyDescriptor.setKeyInfo(keyInfo);
return keyDescriptor;
}

private AssertionConsumerService buildAssertionConsumerService(RelyingPartyRegistration registration) {
AssertionConsumerService acs = build(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
acs.setLocation(registration.getAssertionConsumerServiceLocation());
acs.setBinding(registration.getAssertionConsumerServiceBinding().getUrn());
acs.setIndex(1);
return acs;
}

private SingleLogoutService buildSingleLogoutService(RelyingPartyRegistration registration,
Saml2MessageBinding binding) {
SingleLogoutService slo = build(SingleLogoutService.DEFAULT_ELEMENT_NAME);
slo.setLocation(registration.getSingleLogoutServiceLocation());
slo.setResponseLocation(registration.getSingleLogoutServiceResponseLocation());
slo.setBinding(binding.getUrn());
return slo;
}

private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) {
NameIDFormat nameIdFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME);
nameIdFormat.setURI(registration.getNameIdFormat());
return nameIdFormat;
}

private String serialize(XMLObject xmlObject) {
try {
Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(xmlObject);
if (marshaller == null) {
throw new Saml2Exception("No marshaller found for " + xmlObject.getClass().getName());
}
Element element = marshaller.marshall(xmlObject);
return SerializeSupport.prettyPrintXML(element);
} catch (MarshallingException ex) {
throw new Saml2Exception("Failed to serialize metadata", ex);
}
}

@SuppressWarnings("unchecked")
private <T> T build(QName elementName) {
return (T) XMLObjectProviderRegistrySupport.getBuilderFactory()
.getBuilder(elementName)
.buildObject(elementName);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
import java.util.Collection;

/**
* This class was copied from Spring Security 5.6.0 to get the OpenSaml4AuthenticationProvider to work.
* It should be removed once we are able to more to the spring-security version of OpenSaml4AuthenticationProvider.
* This class was copied from Spring Security 5.6.0 to get the OpenSaml5AuthenticationProvider to work.
* It should be removed once we are able to more to the spring-security version of OpenSaml5AuthenticationProvider.
* <p/>
* Utility methods for decrypting SAML components with OpenSAML
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package org.cloudfoundry.identity.uaa.provider.saml;

import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.shared.resolver.CriteriaSet;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
Expand Down Expand Up @@ -48,8 +48,8 @@
import java.util.Set;

/**
* This class was copied from Spring Security 5.6.0 to get the OpenSaml4AuthenticationProvider to work.
* It should be removed once we are able to more to the spring-security version of OpenSaml4AuthenticationProvider.
* This class was copied from Spring Security 5.6.0 to get the OpenSaml5AuthenticationProvider to work.
* It should be removed once we are able to more to the spring-security version of OpenSaml5AuthenticationProvider.
* <p/>
* Utility methods for verifying SAML component signatures with OpenSAML
* <p>
Expand Down
Loading
Loading