-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Allow RelyingPartyRegistration to be Deduced from Contents of SAML Assertion instead of Path #10243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thanks for the detailed write-up, @vince-recupito. You are correct that I think the idea for an issuer-based resolver has merit -- there is something similar for OAuth 2.0 Resource Servers in I think there are three questions to consider: How would you look up a How would you approach the metadata endpoint ( Finally, how would you handle when the registration id is already known (for example in the case of logout when the registration id is attached to the currently-authenticated user)? |
Digging around in the old SAML library under the following files, it looks like entityId is extracted from the SAML response immediately. Later on in the execution path when the signature gets validated, the entityId appears to be used to locate the proper IDP with its keys. It makes sense to replicate this logic (use entityId from response). It should be fairly easy to extend RelyingPartyRegistrationRepository with a
Our end-goal would be to setup one service provider linked to many IDPs and distribute one set of service provider metadata. In this case we would have a single metadata url with no registration details (/saml2/service-provider-metadata). Furthermore, the metadata itself and the service provider would have no registrationId specific references / endpoints. This was the behavior of the old library. The new library is a bit different from the old library in that you are forced to setup multiple service providers if there is more than one IDP. In this case I wouldn't know which metadata to expose. Perhaps we can give users of the library the option to expose their metadata on /saml2/service-provider-metadata if and only if all registered service providers have the same entityId, decryptionKeys, assertionConsumerService urls, etc (there is effectively only one service provider). If there is more than one effective service provider, we obviously couldn't allow this option as we wouldn't know which SP metadata to expose. Finally, how would you handle when the registration id is already known (for example in the case of logout when the registration id is attached to the currently-authenticated user)? In the case of service provider initiated flows where it is optional to specify the relying party to use (/saml2/authenticate/{registrationId}), it makes sense to leave that as is. Our company doesn't using single-logout and I can't speak to that feature personally as I am not too familiar with it. As a bit of a note: I think a lot of the problem comes from having to register IDP / Service Provider data at the same time as a single RelyingParty. From a modeling perspective, I would have treated the creation of SPs / IDPs independently in the library. So you could register one or more service providers with registration Ids, register one or more IDPs with registration Ids, and then a third mechanism to link certain IDPs to certain SPs. That way SP metadata / endpoints aren't tied to particular IDPs. I'm guessing there were good reasons to go that route though. |
Thanks again, @vince-recupito. I believe you can achieve what you want for logins with the following arrangement: public class OpenSamlResponseRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public OpenSamlResponseRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
String samlResponse = request.getParameter("SAMLResponse");
if (samlResponse == null) {
return null;
}
String entityId = retrieveEntityId(samlResponse);
if (entityId == null) {
return null;
}
return this.delegate.resolve(request, entityId);
}
private String retreiveEntityId(String samlResponse) {
// ...
}
} Then, if the @Bean
Saml2AuthenticationTokenConverter saml2Login(RelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver resolver =
new OpenSamlResponseRelyingPartyRegistrationResolver(registrations);
return new Saml2AuthenticationTokenConverter(resolver);
} or if you have a custom @Bean
Saml2AuthenticationTokenConverter saml2Login(YourRelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver resolver =
new OpenSamlResponseRelyingPartyRegistrationResolver(registrations::findByEntityId);
return new Saml2AuthenticationTokenConverter(resolver);
} Would this address what you are wanting to achieve? |
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed. |
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue. |
I'd like to reopen this issue as I'm facing the same while migrating from the SAML extension. The suggestion in #10243 (comment) would work, however, the implementation of the If implemented on the client side, this is needed: private RelyingPartyRegistration retrieveEntityId(String saml2Response)
{
byte[] b = this.samlDecode(saml2Response);
String samlResponse = new String(b, StandardCharsets.UTF_8);
Response response = parse(samlResponse);
return relyingPartyRegistrationRepository.findByRegistrationId(
response.getIssuer().getValue());
} The trouble is, that the |
Good point, @vladonemo, perhaps there is something else that can be done here. Another idea is to implement public class OpenSamlIssuerAuthenticationTokenConverter implements AuthenticationConverter {
private final Saml2AuthenticationTokenConverter delegate;
private final RelyingPartyRegistrationResolver registrations;
public OpenSamlAuthenticationTokenConverter(RelyingPartyRegistrationResolver registrations) {
this.delegate = new Saml2AuthenticationTokenConverter(registrations);
this.registrations = registrations;
}
@Override
public Authentication convert(HttpServletRequest request) {
Response response = parseSamlResponse(request);
if (response == null) {
return null;
}
Saml2AuthenticationToken token = this.delegate.convert(request);
if (token != null) {
return new OpenSamlAuthenticationToken(
token.getRelyingPartyRegistration(), response, token.getAuthenticationRequest());
}
String registrationId = response.getIssuer();
RelyingPartyRegistration registration = this.registrations.resolve(request, registrationid);
if (registration == null) {
return null;
}
return new OpenSamlAuthenticationToken(registration, response, authenticationRequest);
}
} Hypothetically, |
SAML Service Provider should use the contents of a SAML assertion to deduce the IDP instead of the current Path-based defaults when receiving an assertion. This will enable all IDPs to use a shared, common Service Provider configuration.
Currently, the default implementation provides each IDP a separate AssertionConsumerService url to call the Service Provider ("{baseUrl}/login/saml2/sso/{registrationId}"). As an example, with two IDPs registered, we could have the two urls:
{baseUrl}/login/saml2/sso/idp1
{baseUrl}/login/saml2/sso/idp2
Each IDP gets it's own Service Provider metadata that is specific to their IDP.
Instead, an optional implementation should be to have a static AssertionConsumerService url that all IDPs use, and use the entityId within the SAML assertion to choose the correct IDP to validate against. With the same two IDPs above, we would only have:
{baseUrl}/login/saml2/sso
To enable this approach we could have:
An "AssertionRelyingPartyRegistrationResolver" (in addition to the current, DefaultRelyingPartyRegistrationResolver) that would use the contents of the assertion (entityId) to resolve the correct relying party instead of the path-based route
Some work has already been done on this front to enable different RelyingPartyRegistrationResolvers with the following completed issues:
#8887
#8768
Why is this needed?
I'm new to this library so there might be more to this ticket than what I wrote. In general, we should be able to distribute the same Service Provider metadata (with the same endpoints) to all IDPs. The most obvious violation of this I saw was the AssertionConsumerService url, but perhaps there are more endpoints/values in the metadata that need to be changed in order to support this (Audience Restriction, Recipient Url, etc)?
Also wanted to say thank you for keeping SAML alive in Spring Security Core as the old project is dead!
The text was updated successfully, but these errors were encountered: