Skip to content

Commit

Permalink
Add EntitiesDescriptor Support
Browse files Browse the repository at this point in the history
Closes gh-10782
  • Loading branch information
jzheaux committed Jan 31, 2022
1 parent 9baf113 commit b998e8e
Show file tree
Hide file tree
Showing 6 changed files with 407 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
Expand All @@ -20,6 +20,8 @@
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;
Expand Down Expand Up @@ -63,8 +65,24 @@ class OpenSamlAssertingPartyMetadataConverter {
this.parserPool = this.registry.getParserPool();
}

RelyingPartyRegistration.Builder convert(InputStream inputStream) {
EntityDescriptor descriptor = entityDescriptor(inputStream);
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()) {
builders.add(convert(descriptor));
}
return builders;
}
if (xmlObject instanceof EntityDescriptor) {
EntityDescriptor descriptor = (EntityDescriptor) xmlObject;
return Arrays.asList(convert(descriptor));
}
throw new Saml2Exception("Unsupported element of type " + xmlObject.getClass());
}

RelyingPartyRegistration.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");
Expand Down Expand Up @@ -167,26 +185,19 @@ private List<SigningMethod> signingMethods(IDPSSODescriptor idpssoDescriptor) {
return signingMethods(extensions);
}

private EntityDescriptor entityDescriptor(InputStream inputStream) {
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 {
XMLObject object = unmarshaller.unmarshall(element);
if (object instanceof EntitiesDescriptor) {
return ((EntitiesDescriptor) object).getEntityDescriptors().get(0);
}
if (object instanceof EntityDescriptor) {
return (EntityDescriptor) object;
}
return unmarshaller.unmarshall(element);
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
throw new Saml2Exception("Unsupported element of type " + element.getTagName());
}

private Document document(InputStream inputStream) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
Expand Down Expand Up @@ -89,7 +89,7 @@ public List<MediaType> getSupportedMediaTypes() {
@Override
public RelyingPartyRegistration.Builder read(Class<? extends RelyingPartyRegistration.Builder> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return this.converter.convert(inputMessage.getBody());
return this.converter.convert(inputMessage.getBody()).iterator().next();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
Expand All @@ -18,6 +18,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;

import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
Expand Down Expand Up @@ -122,6 +123,96 @@ public static RelyingPartyRegistration.Builder fromMetadataLocation(String metad
* @since 5.6
*/
public static RelyingPartyRegistration.Builder fromMetadata(InputStream source) {
return assertingPartyMetadataConverter.convert(source).iterator().next();
}

/**
* Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
* of the given SAML 2.0 Asserting Party (IDP) metadata location.
*
* Valid locations can be classpath- or file-based or they can be HTTP endpoints. Some
* valid endpoints might include:
*
* <pre>
* metadataLocation = "classpath:asserting-party-metadata.xml";
* metadataLocation = "file:asserting-party-metadata.xml";
* metadataLocation = "https://ap.example.org/metadata";
* </pre>
*
* Note that by default the registrationId is set to be the given metadata location,
* but this will most often not be sufficient. To complete the configuration, most
* applications will also need to provide a registrationId, like so:
*
* <pre>
* Iterable&lt;RelyingPartyRegistration&gt; registrations = RelyingPartyRegistrations
* .collectionFromMetadataLocation(location).iterator();
* RelyingPartyRegistration one = registrations.next().registrationId("one").build();
* RelyingPartyRegistration two = registrations.next().registrationId("two").build();
* return new InMemoryRelyingPartyRegistrationRepository(one, two);
* </pre>
*
* Also note that an {@code IDPSSODescriptor} typically only contains information
* about the asserting party. Thus, you will need to remember to still populate
* anything about the relying party, like any private keys the relying party will use
* for signing AuthnRequests.
* @param location The classpath- or file-based locations or HTTP endpoints of the
* asserting party metadata file
* @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
* further configuration
* @since 5.7
*/
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadataLocation(String location) {
try (InputStream source = resourceLoader.getResource(location).getInputStream()) {
return collectionFromMetadata(source);
}
catch (IOException ex) {
if (ex.getCause() instanceof Saml2Exception) {
throw (Saml2Exception) ex.getCause();
}
throw new Saml2Exception(ex);
}
}

/**
* Return a {@link Collection} of {@link RelyingPartyRegistration.Builder}s based off
* of the given SAML 2.0 Asserting Party (IDP) metadata.
*
* <p>
* This method is intended for scenarios when the metadata is looked up by a separate
* mechanism. One such example is when the metadata is stored in a database.
* </p>
*
* <p>
* <strong>The callers of this method are accountable for closing the
* {@code InputStream} source.</strong>
* </p>
*
* Note that by default the registrationId is set to be the given metadata location,
* but this will most often not be sufficient. To complete the configuration, most
* applications will also need to provide a registrationId, like so:
*
* <pre>
* String xml = fromDatabase();
* try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
* Iterator&lt;RelyingPartyRegistration&gt; registrations = RelyingPartyRegistrations
* .collectionFromMetadata(source).iterator();
* RelyingPartyRegistration one = registrations.next().registrationId("one").build();
* RelyingPartyRegistration two = registrations.next().registrationId("two").build();
* return new InMemoryRelyingPartyRegistrationRepository(one, two);
* }
* </pre>
*
* Also note that an {@code IDPSSODescriptor} typically only contains information
* about the asserting party. Thus, you will need to remember to still populate
* anything about the relying party, like any private keys the relying party will use
* for signing AuthnRequests.
* @param source the {@link InputStream} source containing the asserting party
* metadata
* @return the {@link Collection} of {@link RelyingPartyRegistration.Builder}s for
* further configuration
* @since 5.7
*/
public static Collection<RelyingPartyRegistration.Builder> collectionFromMetadata(InputStream source) {
return assertingPartyMetadataConverter.convert(source);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
Expand Down Expand Up @@ -98,7 +98,8 @@ public void readWhenDescriptorFullySpecifiedThenConfigures() throws Exception {
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"") + EXTENSIONS_TEMPLATE
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE)));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
RelyingPartyRegistration registration = this.converter.convert(inputStream).iterator().next()
.registrationId("one").build();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getWantAuthnRequestsSigned()).isFalse();
assertThat(details.getSigningAlgorithms()).containsExactly(SignatureConstants.ALGO_ID_DIGEST_SHA512);
Expand All @@ -123,7 +124,8 @@ public void readWhenEntitiesDescriptorThenConfigures() throws Exception {
+ String.format(KEY_DESCRIPTOR_TEMPLATE, "use=\"encryption\"")
+ String.format(SINGLE_SIGN_ON_SERVICE_TEMPLATE))));
InputStream inputStream = new ByteArrayInputStream(payload.getBytes());
RelyingPartyRegistration registration = this.converter.convert(inputStream).registrationId("one").build();
RelyingPartyRegistration registration = this.converter.convert(inputStream).iterator().next()
.registrationId("one").build();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getWantAuthnRequestsSigned()).isFalse();
assertThat(details.getSingleSignOnServiceLocation()).isEqualTo("sso-location");
Expand All @@ -142,7 +144,8 @@ public void readWhenKeyDescriptorHasNoUseThenConfiguresBothKeyTypes() throws Exc
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 registration = this.converter.convert(inputStream).registrationId("one").build();
RelyingPartyRegistration registration = this.converter.convert(inputStream).iterator().next()
.registrationId("one").build();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getVerificationX509Credentials().iterator().next().getCertificate())
.isEqualTo(x509Certificate(CERTIFICATE));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
Expand All @@ -21,6 +21,7 @@
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.stream.Collectors;

import okhttp3.mockwebserver.MockResponse;
Expand All @@ -41,12 +42,18 @@ public class RelyingPartyRegistrationsTests {

private String metadata;

private String entitiesDescriptor;

@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());
}
resource = new ClassPathResource("test-entitiesdescriptor.xml");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
this.entitiesDescriptor = reader.lines().collect(Collectors.joining());
}
}

@Test
Expand Down Expand Up @@ -129,4 +136,111 @@ public void fromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exceptio
}
}

@Test
public void collectionFromMetadataLocationWhenResolvableThenPopulatesBuilder() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(new MockResponse().setBody(this.entitiesDescriptor).setResponseCode(200));
List<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
.collectionFromMetadataLocation(server.url("/").toString()).stream()
.map((r) -> r.entityId("rp").build()).collect(Collectors.toList());
assertThat(registrations).hasSize(2);
RelyingPartyRegistration first = registrations.get(0);
RelyingPartyRegistration.AssertingPartyDetails details = first.getAssertingPartyDetails();
assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(details.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
RelyingPartyRegistration second = registrations.get(1);
details = second.getAssertingPartyDetails();
assertThat(details.getEntityId()).isEqualTo("https://ap.example.org/idp/shibboleth");
assertThat(details.getSingleSignOnServiceLocation())
.isEqualTo("https://ap.example.org/idp/profile/SAML2/POST/SSO");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
}
}

@Test
public void collectionFromMetadataLocationWhenUnresolvableThenSaml2Exception() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200));
String url = server.url("/").toString();
server.shutdown();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url));
}
}

@Test
public void collectionFromMetadataLocationWhenMalformedResponseThenSaml2Exception() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(new MockResponse().setBody("malformed").setResponseCode(200));
String url = server.url("/").toString();
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation(url));
}
}

@Test
public void collectionFromMetadataFileWhenResolvableThenPopulatesBuilder() {
File file = new File("src/test/resources/test-entitiesdescriptor.xml");
RelyingPartyRegistration registration = RelyingPartyRegistrations
.collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream()
.map((r) -> r.entityId("rp").build()).findFirst().get();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(details.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
}

@Test
public void collectionFromMetadataFileWhenContainsOnlyEntityDescriptorThenPopulatesBuilder() {
File file = new File("src/test/resources/test-metadata.xml");
RelyingPartyRegistration registration = RelyingPartyRegistrations
.collectionFromMetadataLocation("file:" + file.getAbsolutePath()).stream()
.map((r) -> r.entityId("rp").build()).findFirst().get();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(details.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
}

@Test
public void collectionFromMetadataFileWhenNotFoundThenSaml2Exception() {
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadataLocation("filePath"));
}

@Test
public void collectionFromMetadataInputStreamWhenResolvableThenPopulatesBuilder() throws Exception {
try (InputStream source = new ByteArrayInputStream(this.entitiesDescriptor.getBytes())) {
RelyingPartyRegistration registration = RelyingPartyRegistrations.collectionFromMetadata(source).stream()
.map((r) -> r.entityId("rp").build()).findFirst().get();
RelyingPartyRegistration.AssertingPartyDetails details = registration.getAssertingPartyDetails();
assertThat(details.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth");
assertThat(details.getSingleSignOnServiceLocation())
.isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO");
assertThat(details.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST);
assertThat(details.getVerificationX509Credentials()).hasSize(1);
assertThat(details.getEncryptionX509Credentials()).hasSize(1);
}
}

@Test
public void collectionFromMetadataInputStreamWhenEmptyThenSaml2Exception() throws Exception {
try (InputStream source = new ByteArrayInputStream("".getBytes())) {
assertThatExceptionOfType(Saml2Exception.class)
.isThrownBy(() -> RelyingPartyRegistrations.collectionFromMetadata(source));
}
}

}
Loading

0 comments on commit b998e8e

Please sign in to comment.