When using the IDP metadata for configuration by RelyingPartyRegistrations#fromMetadataLocation the OpenSamlMetadataAssertingPartyDetailsConverter is converting only the first provided binding for each login and logout, ignoring the other ones.

This makes configuration harder (either all or partly manual) unnecessarily if any other than the first binding is desired. This introduces need to either copy the respective SSO/SLO URLS a.k.a. locations from the metadata descriptor or otherwise obtain them to put them in some config property. Also opens up possibility to diverge from metadata leading to misconfiguration.

To Reproduce use metadata with multiple SSO / SLO bindings, after calling RelyingPartyRegistrations#fromMetadataLocation the resulting builder only contains the first mentioned binding in the xml descriptor.

Expected behavior provide each binding separately to the builder, with one "preselected" (first to mimic current behaviour), in order to make custom choice of binding easier / possible without manual configuration and knowledge of the respective URLS.

Comment From: jzheaux

Thanks for the suggestion, @bitrecycling. The intent is to keep RelyingPartyRegistration as small as possible, given how large the SAML spec is. For example, you've already observed that the registration also assumes there will only be one SSO location and SLO location for the asserting party.

The rule I'd prefer to go by is to add things to RelyingPartyRegistration that Spring Security needs in order to operate. Given that Spring Security always picks the first binding in both cases, there is no need yet to store multiple.

As an escape hatch, Spring Security includes the original OpenSAML EntityDescriptor instance in the asserting party details. You can retrieve it like so:

OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails) registration.getAssertingPartyDetails();
EntityDescriptor descriptor = details.getEntityDescriptor();
// ...

and then proceed to extract the bindings that your application needs.

I'm going to close this as answered; however, please feel free to continue posting if there is more to discuss.

Comment From: bitrecycling

Thank you @jzheaux for the comment and providing a way to work around the issue.

But I really have to disagree here: It's common that a later definition overrides a previous one. But in this case neither RelyingPartyRegistrations nor OpenSamlMetadataAssertingPartyDetailsConverter mentions anything about just taking the first valid binding. Actually OpenSamlMetadataAssertingPartyDetailsConverter does not have any useful comment at all. Also RelyingPartyRegistrations does not give any hint that all but the first binding will just be ignored despite that it is totally legit to have multiple bindings according to SAML spec.

So in short: Please either provide a comment stating the current behaviour and the suggested workaround or extend the converter to adhere to the SAML spec allowing choice / providing multiple bindings (<-- I could provide a solution here)

Comment From: HaroldHormaechea

We faced an issue related to this. We are forcing the RelyingPartyRegistration to use the HTTP-POST binding, but some of our clients may have multiple bindings defined in their IDP. This causes failure, as the first binding may for example contain an URL for HTTP-Redirect, which is not the same as the HTTP-POST one. It seems extremely counter-intuitive that we can set singleSignOnServiceBinding to an arbitrary binding type, but the target URL's are whichever the builder happened to read first.

We have had to do a quite ugly hack to extract the HTTP-POST binding information from the RelyingPartyRegistration.Builder to be able to assign it to itself as its singleSignOnServiceLocation:

          Field providerDetailsField = builder.getClass().getDeclaredField("providerDetails");
          providerDetailsField.setAccessible(true);
          org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.ProviderDetails.Builder
                  providerDetailsBuilder = (RelyingPartyRegistration.ProviderDetails.Builder) providerDetailsField.get(builder);


          Field assertingPartyDetailsBuilderField = providerDetailsBuilder.getClass().getDeclaredField("assertingPartyDetailsBuilder");
          assertingPartyDetailsBuilderField.setAccessible(true);
          OpenSamlAssertingPartyDetails.Builder assertingPartyDetailsBuilder = (OpenSamlAssertingPartyDetails.Builder) assertingPartyDetailsBuilderField.get(providerDetailsBuilder);


          Field assertingPartyDescriptorField = assertingPartyDetailsBuilder.getClass().getDeclaredField("descriptor");
          assertingPartyDescriptorField.setAccessible(true);
          EntityDescriptor descriptor = (EntityDescriptor) assertingPartyDescriptorField.get(assertingPartyDetailsBuilder);
          List<RoleDescriptor> idpSsoDescriptor = descriptor.getRoleDescriptors(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
          return  idpSsoDescriptor
                  .stream()
                  .flatMap(iteratedDescriptor -> ((IDPSSODescriptor) iteratedDescriptor).getSingleSignOnServices().stream())
                  .filter(SingleSignOnServiceImpl.class::isInstance)
                  .map(SingleSignOnServiceImpl.class::cast)
                  .filter(service -> Saml2MessageBinding.POST.getUrn().equals(service.getBinding()))
                  .map(EndpointImpl::getLocation)
                  .findFirst()
                  .orElseThrow(() -> new SAMLConfigurationException("HTTP-POST must be a valid binding in the IDP XML"));

Is there any other proposed alternative to this that does not require to create two registrations, one with the defaults, and then again a second one to pick data from the first one as stated above?