From 3ad6c6ce0691dff3d8598bc2eb5f28d9c7eae215 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 10 Mar 2023 10:41:30 -0700 Subject: [PATCH] Use EntityId-lookup Components Closes gh-12880 --- .../saml2/Saml2LoginConfigurer.java | 22 ++- .../saml2/Saml2LogoutConfigurer.java | 43 +++--- .../saml2/Saml2LoginConfigurerTests.java | 11 +- .../pages/servlet/saml2/login/overview.adoc | 139 ++---------------- ...penSaml4AuthenticationRequestResolver.java | 15 ++ ...OpenSamlAuthenticationRequestResolver.java | 10 +- ...amlAuthenticationRequestResolverTests.java | 22 ++- 7 files changed, 83 insertions(+), 179 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index d9da507d99..1f68a728d2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/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> } } - 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> 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> 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; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 631de332d6..cb020631e2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/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> 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> } 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> 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> } 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 getBeanOrNull(Class clazz) { @@ -385,12 +382,11 @@ public final class Saml2LogoutConfigurer> 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> 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; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 532fe482c0..183220ea68 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/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 diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index c6ba82f33f..e7aab5cf91 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/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 ``] 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 ``] based on the configurations for that `RelyingPartyRegistration` -* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `` and `` 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 ``]; 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 `` and `` 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 <> 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 <> -* You may be <> +* For processing ``s, the `RelyingPartyRegistration` is looked up from the associated `` or from the `` element +* For processing ``s, the `RelyingPartyRegistration` is looked up from the currently logged in user or from the `` element +* For publishing metadata, the `RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable` -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[`` metadata production]. - -[[relyingpartyregistrationresolver-entityid]] -==== Resolving Based on the `` - -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 <>. - -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[`` 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 = 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: - -* <`>> -* <> - -[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]. diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java index 37749b539f..e0c434e21c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java +++ b/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} */ diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java index 0746f60222..0ea5c6ad51 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java +++ b/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)); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java index 9bcb3620b7..457d60ff25 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java +++ b/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();