Przeglądaj źródła

Use EntityId-lookup Components

Closes gh-12880
Josh Cummings 2 lat temu
rodzic
commit
3ad6c6ce06

+ 10 - 12
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

@@ -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.registration.RelyingPartyRegistration;
 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.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.Saml2AuthenticationTokenConverter;
 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) {
 		if (this.relyingPartyRegistrationRepository == null) {
 			this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class);
@@ -339,7 +333,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 			return bean;
 		}
 		OpenSaml4AuthenticationRequestResolver openSaml4AuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver(
-				relyingPartyRegistrationResolver(http));
+				relyingPartyRegistrationRepository(http));
 		openSaml4AuthenticationRequestResolver
 				.setRequestMatcher(new AntPathRequestMatcher(this.authenticationRequestUri));
 		return openSaml4AuthenticationRequestResolver;
@@ -352,10 +346,14 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 		AuthenticationConverter authenticationConverterBean = getBeanOrNull(http,
 				Saml2AuthenticationTokenConverter.class);
 		if (authenticationConverterBean == null) {
-			Assert.state(this.loginProcessingUrl.contains("{registrationId}"),
-					"loginProcessingUrl must contain {registrationId} path variable");
-			return new Saml2AuthenticationTokenConverter(
-					new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository));
+			authenticationConverterBean = getBeanOrNull(http, OpenSamlAuthenticationTokenConverter.class);
+		}
+		if (authenticationConverterBean == null) {
+			OpenSamlAuthenticationTokenConverter converter = new OpenSamlAuthenticationTokenConverter(
+					this.relyingPartyRegistrationRepository);
+			converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http));
+			converter.setRequestMatcher(createLoginProcessingUrlMatcher(this.loginProcessingUrl));
+			return converter;
 		}
 		return authenticationConverterBean;
 	}

+ 19 - 24
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java

@@ -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");
  * 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.registration.RelyingPartyRegistration;
 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.OpenSaml4LogoutRequestResolver;
 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.Saml2LogoutRequestRepository;
 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.logoutSuccessHandler = logout.getLogoutSuccessHandler();
 		}
-		RelyingPartyRegistrationResolver registrations = relyingPartyRegistrationResolver(http);
+		RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
 		http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class);
 		http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.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) {
 		if (this.relyingPartyRegistrationRepository != null) {
 			return this.relyingPartyRegistrationRepository;
@@ -242,18 +236,21 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 	}
 
 	private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter(
-			RelyingPartyRegistrationResolver registrations) {
+			RelyingPartyRegistrationRepository registrations) {
 		LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
 		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);
-		filter.setLogoutRequestMatcher(createLogoutRequestMatcher());
 		filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
 		return postProcess(filter);
 	}
 
 	private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter(
-			RelyingPartyRegistrationResolver registrations) {
+			RelyingPartyRegistrationRepository registrations) {
 		Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations,
 				this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler);
 		logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher());
@@ -261,7 +258,7 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 		return postProcess(logoutResponseFilter);
 	}
 
-	private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) {
+	private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationRepository registrations) {
 		LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
 		Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
 				registrations);
@@ -290,15 +287,15 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 	}
 
 	private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler(
-			RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+			RelyingPartyRegistrationRepository registrations) {
 		Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer
-				.logoutRequestResolver(relyingPartyRegistrationResolver);
+				.logoutRequestResolver(registrations);
 		return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver);
 	}
 
 	private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver(
-			RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
-		return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver);
+			RelyingPartyRegistrationRepository registrations) {
+		return this.logoutResponseConfigurer.logoutResponseResolver(registrations);
 	}
 
 	private <C> C getBeanOrNull(Class<C> clazz) {
@@ -385,12 +382,11 @@ public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
 			return this.logoutRequestValidator;
 		}
 
-		private Saml2LogoutRequestResolver logoutRequestResolver(
-				RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+		private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
 			if (this.logoutRequestResolver != null) {
 				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;
 		}
 
-		private Saml2LogoutResponseResolver logoutResponseResolver(
-				RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
+		private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
 			if (this.logoutResponseResolver == null) {
-				return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
+				return new OpenSaml4LogoutResponseResolver(registrations);
 			}
 			return this.logoutResponseResolver;
 		}

+ 3 - 8
config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java

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

+ 14 - 125
docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc

@@ -666,9 +666,9 @@ In a deployed application, that translates to:
 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/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`
-* `+/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
-* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] 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}+`
+* `+/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/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.
 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.
 
-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>>
-* You may be <<relyingpartyregistrationresolver-entityid, federating many asserting parties>>
+* For processing `<saml2:Response>`s, the `RelyingPartyRegistration` is looked up from the associated `<saml2:AuthRequest>` or from the `<saml2:Response#Issuer>` element
+* 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`.
-The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`.
+When this needs adjustment, you can turn to the specific components for each of these endpoints targeted at customizing this:
 
-[NOTE]
-Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
-
-[[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].
+* For SAML Responses, customize the `AuthenticationConverter`
+* For Logout Requests, customize the `Saml2LogoutRequestValidatorParametersResolver`
+* For Metadata, customize the `Saml2MetadataResponseResolver`
 
 [[federating-saml2-login]]
 === Federating Login
@@ -996,6 +892,7 @@ var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrati
         .stream().map { builder : RelyingPartyRegistration.Builder -> builder
             .registrationId(UUID.randomUUID().toString())
             .entityId("https://example.org/saml2/sp")
+            .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
             .build()
         }
         .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.
 
 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`.
-
-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.
+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.
 
 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].
 

+ 15 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java

@@ -26,6 +26,7 @@ import org.opensaml.saml.saml2.core.AuthnRequest;
 import org.springframework.core.convert.converter.Converter;
 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.RelyingPartyRegistrationRepository;
 import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
@@ -46,6 +47,20 @@ public final class OpenSaml4AuthenticationRequestResolver implements Saml2Authen
 
 	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}
 	 */

+ 8 - 2
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java

@@ -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.registration.RelyingPartyRegistration;
 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.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
@@ -120,15 +122,19 @@ class OpenSamlAuthenticationRequestResolver {
 		if (registration == 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.setForceAuthn(Boolean.FALSE);
 		authnRequest.setIsPassive(Boolean.FALSE);
 		authnRequest.setProtocolBinding(registration.getAssertionConsumerServiceBinding().getUrn());
 		Issuer iss = this.issuerBuilder.buildObject();
-		iss.setValue(registration.getEntityId());
+		iss.setValue(entityId);
 		authnRequest.setIssuer(iss);
 		authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation());
-		authnRequest.setAssertionConsumerServiceURL(registration.getAssertionConsumerServiceLocation());
+		authnRequest.setAssertionConsumerServiceURL(assertionConsumerServiceLocation);
 		authnRequestConsumer.accept(registration, authnRequest);
 		if (authnRequest.getID() == null) {
 			authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));

+ 14 - 8
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java

@@ -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.Saml2MessageBinding;
 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.assertThatExceptionOfType;
@@ -52,13 +54,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
 		RelyingPartyRegistration registration = this.relyingPartyRegistrationBuilder.build();
 		OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
 		Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
+			UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
 			assertThat(authnRequest.getAssertionConsumerServiceURL())
-					.isEqualTo(registration.getAssertionConsumerServiceLocation());
+					.isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
 			assertThat(authnRequest.getProtocolBinding())
 					.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
 			assertThat(authnRequest.getDestination())
 					.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.getRelayState()).isNotNull();
@@ -76,13 +79,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
 				.assertingPartyDetails((party) -> party.wantAuthnRequestsSigned(false)).build();
 		OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
 		Saml2RedirectAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
+			UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
 			assertThat(authnRequest.getAssertionConsumerServiceURL())
-					.isEqualTo(registration.getAssertionConsumerServiceLocation());
+					.isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
 			assertThat(authnRequest.getProtocolBinding())
 					.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
 			assertThat(authnRequest.getDestination())
 					.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.getRelayState()).isNotNull();
@@ -114,13 +118,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
 				.build();
 		OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
 		Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
+			UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
 			assertThat(authnRequest.getAssertionConsumerServiceURL())
-					.isEqualTo(registration.getAssertionConsumerServiceLocation());
+					.isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
 			assertThat(authnRequest.getProtocolBinding())
 					.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
 			assertThat(authnRequest.getDestination())
 					.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.getRelayState()).isNotNull();
@@ -137,13 +142,14 @@ public class OpenSamlAuthenticationRequestResolverTests {
 				.assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build();
 		OpenSamlAuthenticationRequestResolver resolver = authenticationRequestResolver(registration);
 		Saml2PostAuthenticationRequest result = resolver.resolve(request, (r, authnRequest) -> {
+			UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration);
 			assertThat(authnRequest.getAssertionConsumerServiceURL())
-					.isEqualTo(registration.getAssertionConsumerServiceLocation());
+					.isEqualTo(uriResolver.resolve(registration.getAssertionConsumerServiceLocation()));
 			assertThat(authnRequest.getProtocolBinding())
 					.isEqualTo(registration.getAssertionConsumerServiceBinding().getUrn());
 			assertThat(authnRequest.getDestination())
 					.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.getRelayState()).isNotNull();