Extract EntityDescriptor to AssertingPartyDetails Logic

Closes gh-15090
This commit is contained in:
Josh Cummings 2024-07-10 16:56:14 -06:00
parent c885cee719
commit dcf5cc9e06
No known key found for this signature in database
GPG Key ID: A306A51F43B8E5A5
7 changed files with 231 additions and 451 deletions

View File

@ -16,12 +16,25 @@
package org.springframework.security.saml2.provider.service.registration;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.ext.saml2alg.SigningMethod;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.Extensions;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.Saml2X509Credential;
/**
@ -62,7 +75,111 @@ public final class OpenSamlAssertingPartyDetails extends RelyingPartyRegistratio
* for further configurations
*/
public static OpenSamlAssertingPartyDetails.Builder withEntityDescriptor(EntityDescriptor entity) {
return new OpenSamlAssertingPartyDetails.Builder(entity);
IDPSSODescriptor idpssoDescriptor = entity.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
if (idpssoDescriptor == null) {
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
}
List<Saml2X509Credential> verification = new ArrayList<>();
List<Saml2X509Credential> encryption = new ArrayList<>();
for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
verification.add(Saml2X509Credential.verification(certificate));
}
}
if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
encryption.add(Saml2X509Credential.encryption(certificate));
}
}
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
verification.add(Saml2X509Credential.verification(certificate));
encryption.add(Saml2X509Credential.encryption(certificate));
}
}
}
if (verification.isEmpty()) {
throw new Saml2Exception(
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
}
OpenSamlAssertingPartyDetails.Builder builder = new OpenSamlAssertingPartyDetails.Builder(entity)
.entityId(entity.getEntityID())
.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
.verificationX509Credentials((c) -> c.addAll(verification))
.encryptionX509Credentials((c) -> c.addAll(encryption));
List<SigningMethod> signingMethods = signingMethods(idpssoDescriptor);
for (SigningMethod method : signingMethods) {
builder.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm()));
}
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
throw new Saml2Exception(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
}
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
Saml2MessageBinding binding;
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
binding = Saml2MessageBinding.POST;
}
else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
binding = Saml2MessageBinding.REDIRECT;
}
else {
continue;
}
builder.singleSignOnServiceLocation(singleSignOnService.getLocation()).singleSignOnServiceBinding(binding);
break;
}
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
Saml2MessageBinding binding;
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
binding = Saml2MessageBinding.POST;
}
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
binding = Saml2MessageBinding.REDIRECT;
}
else {
continue;
}
String responseLocation = (singleLogoutService.getResponseLocation() == null)
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
builder.singleLogoutServiceLocation(singleLogoutService.getLocation())
.singleLogoutServiceResponseLocation(responseLocation)
.singleLogoutServiceBinding(binding);
break;
}
return builder;
}
private static List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
try {
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
}
catch (CertificateException ex) {
throw new Saml2Exception(ex);
}
}
private static List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) {
Extensions extensions = idpssoDescriptor.getExtensions();
List<SigningMethod> result = signingMethods(extensions);
if (!result.isEmpty()) {
return result;
}
EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent();
extensions = descriptor.getExtensions();
return signingMethods(extensions);
}
private static <T> List<T> signingMethods(Extensions extensions) {
if (extensions != null) {
return (List<T>) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME);
}
return new ArrayList<>();
}
@Override

View File

@ -1,228 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.saml2.provider.service.registration;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import org.opensaml.core.config.ConfigurationService;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
import org.opensaml.core.xml.io.Unmarshaller;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.ext.saml2alg.SigningMethod;
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.Extensions;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml.saml2.metadata.SingleSignOnService;
import org.opensaml.security.credential.UsageType;
import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
import org.springframework.security.saml2.core.Saml2X509Credential;
class OpenSamlMetadataRelyingPartyRegistrationConverter {
static {
OpenSamlInitializationService.initialize();
}
private final XMLObjectProviderRegistry registry;
private final ParserPool parserPool;
/**
* Creates a {@link OpenSamlMetadataRelyingPartyRegistrationConverter}
*/
OpenSamlMetadataRelyingPartyRegistrationConverter() {
this.registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
this.parserPool = this.registry.getParserPool();
}
OpenSamlRelyingPartyRegistration.Builder convert(EntityDescriptor descriptor) {
IDPSSODescriptor idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
if (idpssoDescriptor == null) {
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
}
List<Saml2X509Credential> verification = new ArrayList<>();
List<Saml2X509Credential> encryption = new ArrayList<>();
for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) {
if (keyDescriptor.getUse().equals(UsageType.SIGNING)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
verification.add(Saml2X509Credential.verification(certificate));
}
}
if (keyDescriptor.getUse().equals(UsageType.ENCRYPTION)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
encryption.add(Saml2X509Credential.encryption(certificate));
}
}
if (keyDescriptor.getUse().equals(UsageType.UNSPECIFIED)) {
List<X509Certificate> certificates = certificates(keyDescriptor);
for (X509Certificate certificate : certificates) {
verification.add(Saml2X509Credential.verification(certificate));
encryption.add(Saml2X509Credential.encryption(certificate));
}
}
}
if (verification.isEmpty()) {
throw new Saml2Exception(
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
}
OpenSamlRelyingPartyRegistration.Builder builder = OpenSamlRelyingPartyRegistration
.withAssertingPartyEntityDescriptor(descriptor)
.assertingPartyDetails((party) -> party.entityId(descriptor.getEntityID())
.wantAuthnRequestsSigned(Boolean.TRUE.equals(idpssoDescriptor.getWantAuthnRequestsSigned()))
.verificationX509Credentials((c) -> c.addAll(verification))
.encryptionX509Credentials((c) -> c.addAll(encryption)));
List<SigningMethod> signingMethods = signingMethods(idpssoDescriptor);
for (SigningMethod method : signingMethods) {
builder.assertingPartyDetails(
(party) -> party.signingAlgorithms((algorithms) -> algorithms.add(method.getAlgorithm())));
}
if (idpssoDescriptor.getSingleSignOnServices().isEmpty()) {
throw new Saml2Exception(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
}
for (SingleSignOnService singleSignOnService : idpssoDescriptor.getSingleSignOnServices()) {
Saml2MessageBinding binding;
if (singleSignOnService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
binding = Saml2MessageBinding.POST;
}
else if (singleSignOnService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
binding = Saml2MessageBinding.REDIRECT;
}
else {
continue;
}
builder
.assertingPartyDetails((party) -> party.singleSignOnServiceLocation(singleSignOnService.getLocation())
.singleSignOnServiceBinding(binding));
break;
}
for (SingleLogoutService singleLogoutService : idpssoDescriptor.getSingleLogoutServices()) {
Saml2MessageBinding binding;
if (singleLogoutService.getBinding().equals(Saml2MessageBinding.POST.getUrn())) {
binding = Saml2MessageBinding.POST;
}
else if (singleLogoutService.getBinding().equals(Saml2MessageBinding.REDIRECT.getUrn())) {
binding = Saml2MessageBinding.REDIRECT;
}
else {
continue;
}
String responseLocation = (singleLogoutService.getResponseLocation() == null)
? singleLogoutService.getLocation() : singleLogoutService.getResponseLocation();
builder
.assertingPartyDetails((party) -> party.singleLogoutServiceLocation(singleLogoutService.getLocation())
.singleLogoutServiceResponseLocation(responseLocation)
.singleLogoutServiceBinding(binding));
break;
}
return builder;
}
Collection<RelyingPartyRegistration.Builder> convert(InputStream inputStream) {
List<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
XMLObject xmlObject = xmlObject(inputStream);
if (xmlObject instanceof EntitiesDescriptor) {
EntitiesDescriptor descriptors = (EntitiesDescriptor) xmlObject;
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
builders.add(convert(descriptor));
}
}
if (builders.isEmpty()) {
throw new Saml2Exception("Metadata contains no IDPSSODescriptor elements");
}
return builders;
}
if (xmlObject instanceof EntityDescriptor) {
EntityDescriptor descriptor = (EntityDescriptor) xmlObject;
return Arrays.asList(convert(descriptor));
}
throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass());
}
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
try {
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
}
catch (CertificateException ex) {
throw new Saml2Exception(ex);
}
}
private List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) {
Extensions extensions = idpssoDescriptor.getExtensions();
List<SigningMethod> result = signingMethods(extensions);
if (!result.isEmpty()) {
return result;
}
EntityDescriptor descriptor = (EntityDescriptor) idpssoDescriptor.getParent();
extensions = descriptor.getExtensions();
return signingMethods(extensions);
}
private XMLObject xmlObject(InputStream inputStream) {
Document document = document(inputStream);
Element element = document.getDocumentElement();
Unmarshaller unmarshaller = this.registry.getUnmarshallerFactory().getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
try {
return unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private Document document(InputStream inputStream) {
try {
return this.parserPool.parse(inputStream);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
private <T> List<T> signingMethods(Extensions extensions) {
if (extensions != null) {
return (List<T>) extensions.getUnknownXMLObjects(SigningMethod.DEFAULT_ELEMENT_NAME);
}
return new ArrayList<>();
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.saml2.provider.service.registration;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
import org.opensaml.core.xml.io.Unmarshaller;
import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.security.saml2.core.OpenSamlInitializationService;
final class OpenSamlMetadataUtils {
static {
OpenSamlInitializationService.initialize();
}
private OpenSamlMetadataUtils() {
}
static Collection<EntityDescriptor> descriptors(InputStream metadata) {
XMLObject object = xmlObject(metadata);
if (object instanceof EntityDescriptor descriptor) {
return Collections.singleton(descriptor);
}
if (object instanceof EntitiesDescriptor descriptors) {
return descriptors.getEntityDescriptors();
}
throw new Saml2Exception("Unsupported element type: " + object.getClass().getName());
}
static XMLObject xmlObject(InputStream inputStream) {
Document document = document(inputStream);
Element element = document.getDocumentElement();
Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(element);
if (unmarshaller == null) {
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}
try {
return unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
static Document document(InputStream inputStream) {
try {
return XMLObjectProviderRegistrySupport.getParserPool().parse(inputStream);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -62,15 +62,6 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
OpenSamlInitializationService.initialize();
}
private final OpenSamlMetadataRelyingPartyRegistrationConverter converter;
/**
* Creates a {@link OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter}
*/
public OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter() {
this.converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return RelyingPartyRegistration.Builder.class.isAssignableFrom(clazz);
@ -89,7 +80,7 @@ public class OpenSamlRelyingPartyRegistrationBuilderHttpMessageConverter
@Override
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return this.converter.convert(inputMessage.getBody()).iterator().next();
return RelyingPartyRegistrations.fromMetadata(inputMessage.getBody());
}
@Override

View File

@ -18,8 +18,12 @@ package org.springframework.security.saml2.provider.service.registration;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.saml2.Saml2Exception;
@ -34,8 +38,6 @@ import org.springframework.security.saml2.Saml2Exception;
*/
public final class RelyingPartyRegistrations {
private static final OpenSamlMetadataRelyingPartyRegistrationConverter relyingPartyRegistrationConverter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
private static final ResourceLoader resourceLoader = new DefaultResourceLoader();
private RelyingPartyRegistrations() {
@ -213,7 +215,19 @@ public final class RelyingPartyRegistrations {
* @since 5.7
*/
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
return relyingPartyRegistrationConverter.convert(source);
Collection<RelyingPartyRegistration.Builder> builders = new ArrayList<>();
for (EntityDescriptor descriptor : OpenSamlMetadataUtils.descriptors(source)) {
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
OpenSamlAssertingPartyDetails assertingParty = OpenSamlAssertingPartyDetails
.withEntityDescriptor(descriptor)
.build();
builders.add(new OpenSamlRelyingPartyRegistration.Builder(assertingParty));
}
}
if (builders.isEmpty()) {
throw new Saml2Exception("Metadata response is missing the necessary IDPSSODescriptor element");
}
return builders;
}
}

View File

@ -1,208 +0,0 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* 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
*
* https://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 org.springframework.security.saml2.provider.service.registration;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.saml2.Saml2Exception;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class OpenSamlMetadataRelyingPartyRegistrationConverterTests {
private static final String CERTIFICATE = "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYDVQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwXc2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0BwaXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAaBgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQDDBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlrQHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduOnRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+vZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLuxbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6zV9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk";
private static final String ENTITIES_DESCRIPTOR_TEMPLATE = "<md:EntitiesDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\">\n%s</md:EntitiesDescriptor>";
private static final String ENTITY_DESCRIPTOR_TEMPLATE = "<md:EntityDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" "
+ "xmlns:alg=\"urn:oasis:names:tc:SAML:metadata:algsupport\" " + "entityID=\"entity-id\" "
+ "ID=\"_bf133aac099b99b3d81286e1a341f2d34188043a77fe15bf4bf1487dae9b2ea3\">\n%s"
+ "</md:EntityDescriptor>";
private static final String IDP_SSO_DESCRIPTOR_TEMPLATE = "<md:IDPSSODescriptor protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n"
+ "%s\n" + "</md:IDPSSODescriptor>";
private static final String KEY_DESCRIPTOR_TEMPLATE = "<md:KeyDescriptor %s>\n"
+ "<ds:KeyInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n" + "<ds:X509Data>\n"
+ "<ds:X509Certificate>" + CERTIFICATE + "</ds:X509Certificate>\n" + "</ds:X509Data>\n" + "</ds:KeyInfo>\n"
+ "</md:KeyDescriptor>";
private static final String EXTENSIONS_TEMPLATE = "<md:Extensions>" + "<alg:SigningMethod Algorithm=\""
+ SignatureConstants.ALGO_ID_DIGEST_SHA512 + "\"/>" + "</md:Extensions>";
private static final String SINGLE_SIGN_ON_SERVICE_TEMPLATE = "<md:SingleSignOnService Binding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect\" "
+ "Location=\"sso-location\"/>";
private OpenSamlMetadataRelyingPartyRegistrationConverter converter = new OpenSamlMetadataRelyingPartyRegistrationConverter();
private String metadata;
@BeforeEach
public void setup() throws Exception {
ClassPathResource resource = new ClassPathResource("test-metadata.xml");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
this.metadata = reader.lines().collect(Collectors.joining());
}
}
// gh-12667
@Test
public void convertWhenDefaultsThenAssertingPartyInstanceOfOpenSaml() throws Exception {
try (InputStream source = new ByteArrayInputStream(this.metadata.getBytes(StandardCharsets.UTF_8))) {
this.converter.convert(source)
.forEach((registration) -> assertThat(registration.build().getAssertingPartyDetails())
.isInstanceOf(OpenSamlAssertingPartyDetails.class));
}
}
@Test
public void readWhenMissingIDPSSODescriptorThenException() {
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, "");
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
.withMessageContaining("Metadata response is missing the necessary IDPSSODescriptor element");
}
@Test
public void readWhenMissingVerificationKeyThenException() {
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, ""));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
.withMessageContaining(
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
}
@Test
public void readWhenMissingSingleSignOnServiceThenException() {
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE, String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
.withMessageContaining(
"Metadata response is missing a SingleSignOnService, necessary for sending AuthnRequests");
}
@Test
public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE,
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream)
.iterator()
.next()
.build()
.getAssertingPartyDetails();
assertThat(details.getWantAuthnRequestsSigned()).isFalse();
assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(details.getEntityId()).isEqualTo("entity-id");
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
assertThat(details).isInstanceOf(OpenSamlAssertingPartyDetails.class);
OpenSamlAssertingPartyDetails openSamlDetails = (OpenSamlAssertingPartyDetails) details;
EntityDescriptor entityDescriptor = openSamlDetails.getEntityDescriptor();
assertThat(entityDescriptor).isNotNull();
assertThat(entityDescriptor.getEntityID()).isEqualTo(details.getEntityId());
}
// gh-9051
@Test
public void readWhenEntitiesDescriptorThenConfigures() throws Exception {
String payload = String.format(ENTITIES_DESCRIPTOR_TEMPLATE,
String.format(ENTITY_DESCRIPTOR_TEMPLATE,
String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"signing\"")
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"")
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream)
.iterator()
.next()
.build()
.getAssertingPartyDetails();
assertThat(details.getWantAuthnRequestsSigned()).isFalse();
assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
assertThat(details.getEntityId()).isEqualTo("entity-id");
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
}
@Test
public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exception {
String payload = String.format(ENTITY_DESCRIPTOR_TEMPLATE, String.format(IDP_SSO_DESCRIPTOR_TEMPLATE,
String.format(KEY_DESCRIPTOR_TEMPLATE, "") + String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
RelyingPartyRegistration.AssertingPartyDetails details = this.converter.convert(inputStream)
.iterator()
.next()
.build()
.getAssertingPartyDetails();
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
}
X509Certificate x509Certificate(String data) {
try {
InputStream certificate = new ByteArrayInputStream(Base64.getDecoder().decode(data.getBytes()));
return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(certificate);
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
// gh-9051
@Test
public void readWhenUnsupportedElementThenSaml2Exception() {
String payload = "<saml2:Assertion xmlns:saml2=\"https://some.endpoint\"/>";
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.converter.convert(inputStream))
.withMessage("Unsupported element of type saml2:Assertion");
}
}

View File

@ -252,6 +252,21 @@ public class RelyingPartyRegistrationsTests {
}
}
@Test
public void fromMetadataLocationWhenResolvableThenUsesEntityIdAndOpenSamlRelyingPartyRegistration()
throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200));
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(server.url("/").toString())
.entityId("rp")
.build();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(registration.getRegistrationId()).isEqualTo(details.getEntityId());
assertThat(registration).isInstanceOf(OpenSamlRelyingPartyRegistration.class);
}
}
@Test
public void collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception {
try (InputStream source = new ByteArrayInputStream("".getBytes())) {