Use EntityId-lookup Components

Closes gh-12880
This commit is contained in:
Josh Cummings 2023-03-10 10:41:30 -07:00
parent dbdf04f151
commit 3ad6c6ce06
7 changed files with 83 additions and 179 deletions

View File

@ -33,9 +33,8 @@ import org.springframework.security.saml2.provider.service.authentication.Abstra
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.OpenSamlAuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter;
import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter; import org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter;
@ -292,11 +291,6 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
} }
} }
private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(B http) {
RelyingPartyRegistrationRepository registrations = relyingPartyRegistrationRepository(http);
return new DefaultRelyingPartyRegistrationResolver(registrations);
}
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) { RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) {
if (this.relyingPartyRegistrationRepository == null) { if (this.relyingPartyRegistrationRepository == null) {
this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
@ -339,7 +333,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
return bean; return bean;
} }
OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver(
relyingPartyRegistrationResolver(http)); relyingPartyRegistrationRepository(http));
openSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver
.setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri)); .setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri));
return openSaml4AuthenticationRequestResolver; return openSaml4AuthenticationRequestResolver;
@ -352,10 +346,14 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
AuthenticationConverter authenticationConverterBean = getBeanOrNull(http, AuthenticationConverter authenticationConverterBean = getBeanOrNull(http,
Saml2AuthenticationTokenConverter.class); Saml2AuthenticationTokenConverter.class);
if (authenticationConverterBean == null) { if (authenticationConverterBean == null) {
Assert.state(this.loginProcessingUrl.contains("{registrationId}"), authenticationConverterBean = getBeanOrNull(http, OpenSamlAuthenticationTokenConverter.class);
"loginProcessingUrl must contain {registrationId} path variable"); }
return new Saml2AuthenticationTokenConverter( if (authenticationConverterBean == null) {
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)); OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(
this.relyingPartyRegistrationRepository);
converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
converter.setRequestMatcher(createLoginProcessingUrlMatcher(this.loginProcessingUrl));
return converter;
} }
return authenticationConverterBean; return authenticationConverterBean;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2023 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -39,11 +39,10 @@ import org.springframework.security.saml2.provider.service.authentication.logout
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestValidatorParametersResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
@ -216,17 +215,12 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
this.logoutHandlers = logout.getLogoutHandlers(); this.logoutHandlers = logout.getLogoutHandlers();
this.logoutSuccessHandler = logout.getLogoutSuccessHandler(); this.logoutSuccessHandler = logout.getLogoutSuccessHandler();
} }
RelyingPartyRegistrationResolver registrations = relyingPartyRegistrationResolver(http); RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class); http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class);
http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class); http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class);
http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class); http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class);
} }
private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(H http) {
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
return new DefaultRelyingPartyRegistrationResolver(registrations);
}
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {
if (this.relyingPartyRegistrationRepository != null) { if (this.relyingPartyRegistrationRepository != null) {
return this.relyingPartyRegistrationRepository; return this.relyingPartyRegistrationRepository;
@ -242,18 +236,21 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
} }
private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter( private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter(
RelyingPartyRegistrationResolver registrations) { RelyingPartyRegistrationRepository registrations) {
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations); Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations);
Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(registrations, RequestMatcher requestMatcher = createLogoutRequestMatcher();
OpenSamlLogoutRequestValidatorParametersResolver parameters = new OpenSamlLogoutRequestValidatorParametersResolver(
registrations);
parameters.setRequestMatcher(requestMatcher);
Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(parameters,
this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers); this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers);
filter.setLogoutRequestMatcher(createLogoutRequestMatcher());
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
return postProcess(filter); return postProcess(filter);
} }
private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter( private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter(
RelyingPartyRegistrationResolver registrations) { RelyingPartyRegistrationRepository registrations) {
Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations, Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations,
this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler); this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler);
logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher()); logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher());
@ -261,7 +258,7 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
return postProcess(logoutResponseFilter); return postProcess(logoutResponseFilter);
} }
private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) { private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationRepository registrations) {
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler( Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
registrations); registrations);
@ -290,15 +287,15 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
} }
private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler( private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler(
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { RelyingPartyRegistrationRepository registrations) {
Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer
.logoutRequestResolver(relyingPartyRegistrationResolver); .logoutRequestResolver(registrations);
return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver); return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver);
} }
private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver( private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver(
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { RelyingPartyRegistrationRepository registrations) {
return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver); return this.logoutResponseConfigurer.logoutResponseResolver(registrations);
} }
private <C> C getBeanOrNull(Class<C> clazz) { private <C> C getBeanOrNull(Class<C> clazz) {
@ -385,12 +382,11 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
return this.logoutRequestValidator; return this.logoutRequestValidator;
} }
private Saml2LogoutRequestResolver logoutRequestResolver( private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
if (this.logoutRequestResolver != null) { if (this.logoutRequestResolver != null) {
return this.logoutRequestResolver; return this.logoutRequestResolver;
} }
return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver); return new OpenSaml4LogoutRequestResolver(registrations);
} }
} }
@ -454,10 +450,9 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
return this.logoutResponseValidator; return this.logoutResponseValidator;
} }
private Saml2LogoutResponseResolver logoutResponseResolver( private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
if (this.logoutResponseResolver == null) { if (this.logoutResponseResolver == null) {
return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver); return new OpenSaml4LogoutResponseResolver(registrations);
} }
return this.logoutResponseResolver; return this.logoutResponseResolver;
} }

View File

@ -31,7 +31,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -91,7 +90,6 @@ import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.atLeastOnce;
@ -308,12 +306,9 @@ public class Saml2LoginConfigurerTests {
} }
@Test @Test
public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenValidates() { public void saml2LoginWhenLoginProcessingUrlWithoutRegistrationIdAndDefaultAuthenticationConverterThenAutowires()
assertThatExceptionOfType(BeanCreationException.class) throws Exception {
.isThrownBy(() -> this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class) this.spring.register(CustomLoginProcessingUrlDefaultAuthenticationConverter.class).autowire();
.autowire())
.havingRootCause().isInstanceOf(IllegalStateException.class)
.withMessage("loginProcessingUrl must contain {registrationId} path variable");
} }
@Test @Test

View File

@ -666,9 +666,9 @@ In a deployed application, that translates to:
The prevailing URI patterns are as follows: The prevailing URI patterns are as follows:
* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party * `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a `<saml2:AuthnRequest>`] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party
* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`] based on the configurations for that `RelyingPartyRegistration` * `+/login/saml2/sso/+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's `<saml2:Response>`]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the response's issuer if needed; also supports `+/login/saml2/sso/{registrationId}+`
* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state * `+/logout/saml2/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `<saml2:LogoutRequest>` and `<saml2:LogoutResponse>` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state or the request's issuer if needed; also supports `+/logout/saml2/slo/{registrationId}+`
* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration` * `+/saml2/metadata+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for the set of `RelyingPartyRegistration`s; also supports `+/saml2/metadata/{registrationId}+` or `+/saml2/service-provider-metadata/{registrationId}+` for a specific `RelyingPartyRegistration`
Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios. Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios.
If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`. If you wish to remove the `registrationId` from the URL for any reason, you can <<servlet-saml2login-rpr-relyingpartyregistrationresolver,specify a `RelyingPartyRegistrationResolver`>> to tell Spring Security how to look up the `registrationId`.
@ -849,122 +849,18 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path. As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
There are a number of reasons you may want to customize that. Among them: Depending on the use case, a number of other strategies are also employed to derive one.
For example:
* You may already <<relyingpartyregistrationresolver-single, know which `RelyingPartyRegistration` you need>> * For processing `<saml2:Response>`s, the `RelyingPartyRegistration` is looked up from the associated `<saml2:AuthRequest>` or from the `<saml2:Response#Issuer>` element
* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>> * For processing `<saml2:LogoutRequest>`s, the `RelyingPartyRegistration` is looked up from the currently logged in user or from the `<saml2:LogoutRequest#Issuer>` element
* For publishing metadata, the `RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable<RelyingPartyRegistration>`
To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`. When this needs adjustment, you can turn to the specific components for each of these endpoints targeted at customizing this:
The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
[NOTE] * For SAML Responses, customize the `AuthenticationConverter`
Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them. * For Logout Requests, customize the `Saml2LogoutRequestValidatorParametersResolver`
* For Metadata, customize the `Saml2MetadataResponseResolver`
[[relyingpartyregistrationresolver-single]]
==== Resolving to a Single Consistent `RelyingPartyRegistration`
You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`:
====
.Java
[source,java,role="primary"]
----
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
@Override
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
}
}
----
====
[TIP]
You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`<saml2:SPSSODescriptor>` metadata production].
[[relyingpartyregistrationresolver-entityid]]
==== Resolving Based on the `<saml2:Response#Issuer>`
When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <<servlet-saml2login-rpr-duplicated, relying party information duplicated across each instance>>.
This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable.
You can instead resolve the `registrationId` via the `Issuer`.
A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like:
====
.Java
[source,java,role="primary"]
----
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final InMemoryRelyingPartyRegistrationRepository registrations;
// ... constructor
@Override
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId);
}
String entityId = resolveEntityIdFromSamlResponse(request);
for (RelyingPartyRegistration registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration;
}
}
return null;
}
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
// ...
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
RelyingPartyRegistrationResolver {
@Override
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId)
}
String entityId = resolveEntityIdFromSamlResponse(request)
for (val registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration
}
}
return null
}
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
// ...
}
}
----
====
[TIP]
You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`<saml2:Response>` authentication].
[[federating-saml2-login]] [[federating-saml2-login]]
=== Federating Login === Federating Login
@ -996,6 +892,7 @@ var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrati
.stream().map { builder : RelyingPartyRegistration.Builder -> builder .stream().map { builder : RelyingPartyRegistration.Builder -> builder
.registrationId(UUID.randomUUID().toString()) .registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp") .entityId("https://example.org/saml2/sp")
.assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
.build() .build()
} }
.collect(Collectors.toList())); .collect(Collectors.toList()));
@ -1006,15 +903,7 @@ Note that because the registration id is set to a random value, this will change
There are several ways to address this; let's focus on a way that suits the specific use case of federation. There are several ways to address this; let's focus on a way that suits the specific use case of federation.
In many federation cases, all the asserting parties share service provider configuration. In many federation cases, all the asserting parties share service provider configuration.
Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`. Given that Spring Security will by default include the `registrationId` in the service provider metadata, another step is to change corresponding URIs to exclude the `registrationId`, which you can see has already been done in the above sample where the `entityId` and `assertionConsumerServiceLocation` are configured with a static endpoint.
There are two main URIs you will want to change along those lines:
* <<relyingpartyregistrationresolver-entityid,Resolve by `<saml2:Response#Issuer>`>>
* <<relyingpartyregistrationresolver-single,Resolve with a default `RelyingPartyRegistration`>>
[NOTE]
Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal.
You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample]. You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample].

View File

@ -26,6 +26,7 @@ import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -46,6 +47,20 @@ public final class OpenSaml4AuthenticationRequestResolver implements Saml2Authen
private Clock clock = Clock.systemUTC(); private Clock clock = Clock.systemUTC();
/**
* Construct an {@link OpenSaml4AuthenticationRequestResolver}
* @param registrations a repository for relying and asserting party configuration
* @since 6.1
*/
public OpenSaml4AuthenticationRequestResolver(RelyingPartyRegistrationRepository registrations) {
this.authnRequestResolver = new OpenSamlAuthenticationRequestResolver((request, id) -> {
if (id == null) {
return null;
}
return registrations.findByRegistrationId(id);
});
}
/** /**
* Construct a {@link OpenSaml4AuthenticationRequestResolver} * Construct a {@link OpenSaml4AuthenticationRequestResolver}
*/ */

View File

@ -45,6 +45,8 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2P
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
@ -120,15 +122,19 @@ class OpenSamlAuthenticationRequestResolver {
if (registration == null) { if (registration == null) {
return null; return null;
} }
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
String entityId = uriResolver.resolve(registration.getEntityId());
String assertionConsumerServiceLocation = uriResolver
.resolve(registration.getAssertionConsumerServiceLocation());
AuthnRequest authnRequest = this.authnRequestBuilder.buildObject(); AuthnRequest authnRequest = this.authnRequestBuilder.buildObject();
authnRequest.setForceAuthn(Boolean.FALSE); authnRequest.setForceAuthn(Boolean.FALSE);
authnRequest.setIsPassive(Boolean.FALSE); authnRequest.setIsPassive(Boolean.FALSE);
authnRequest.setProtocolBinding(registration.getAssertionConsumerServiceBinding().getUrn()); authnRequest.setProtocolBinding(registration.getAssertionConsumerServiceBinding().getUrn());
Issuer iss = this.issuerBuilder.buildObject(); Issuer iss = this.issuerBuilder.buildObject();
iss.setValue(registration.getEntityId()); iss.setValue(entityId);
authnRequest.setIssuer(iss); authnRequest.setIssuer(iss);
authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation()); authnRequest.setAssertionConsumerServiceURL(assertionConsumerServiceLocation);
authnRequestConsumer.accept(registration, authnRequest); authnRequestConsumer.accept(registration, authnRequest);
if (authnRequest.getID() == null) { if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));

View File

@ -29,6 +29,8 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2R
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -52,13 +54,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build(); RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
assertThat(authnRequest.getAssertionConsumerServiceURL()) assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation()); .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
assertThat(authnRequest.getProtocolBinding()) assertThat(authnRequest.getProtocolBinding())
.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
assertThat(authnRequest.getDestination()) assertThat(authnRequest.getDestination())
.isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId()));
}); });
assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNotNull(); assertThat(result.getRelayState()).isNotNull();
@ -76,13 +79,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build(); .assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
assertThat(authnRequest.getAssertionConsumerServiceURL()) assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation()); .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
assertThat(authnRequest.getProtocolBinding()) assertThat(authnRequest.getProtocolBinding())
.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
assertThat(authnRequest.getDestination()) assertThat(authnRequest.getDestination())
.isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId()));
}); });
assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNotNull(); assertThat(result.getRelayState()).isNotNull();
@ -114,13 +118,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
.build(); .build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
assertThat(authnRequest.getAssertionConsumerServiceURL()) assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation()); .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
assertThat(authnRequest.getProtocolBinding()) assertThat(authnRequest.getProtocolBinding())
.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
assertThat(authnRequest.getDestination()) assertThat(authnRequest.getDestination())
.isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId()));
}); });
assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNotNull(); assertThat(result.getRelayState()).isNotNull();
@ -137,13 +142,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build(); .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration); OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> { Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
assertThat(authnRequest.getAssertionConsumerServiceURL()) assertThat(authnRequest.getAssertionConsumerServiceURL())
.isEqualTo(registration.getAssertionConsumerServiceLocation()); .isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
assertThat(authnRequest.getProtocolBinding()) assertThat(authnRequest.getProtocolBinding())
.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn()); .isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
assertThat(authnRequest.getDestination()) assertThat(authnRequest.getDestination())
.isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
assertThat(authnRequest.getIssuer().getValue()).isEqualTo(registration.getEntityId()); assertThat(authnRequest.getIssuer().getValue()).isEqualTo(uriResolver.resolve(registration.getEntityId()));
}); });
assertThat(result.getSamlRequest()).isNotEmpty(); assertThat(result.getSamlRequest()).isNotEmpty();
assertThat(result.getRelayState()).isNotNull(); assertThat(result.getRelayState()).isNotNull();