diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc index be2fd9ba05..1971c6b589 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc @@ -555,24 +555,19 @@ There are a number of reasons you may want to customize. Among them: * You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme * You may identify tenants in a way other than by the URI path -To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`. +To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `Converter`. The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`. You can provide a simpler resolver that, for example, always returns the same relying party: [source,java] ---- -public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { - - private final RelyingPartyRegistrationResolver delegate; - - public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) { - this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations); - } +public class SingleRelyingPartyRegistrationResolver + implements Converter { @Override - public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { - return this.delegate.resolve(request, "single"); + public RelyingPartyRegistration convert(HttpServletRequest request) { + return this.relyingParty; } } ---- @@ -1020,7 +1015,7 @@ You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the f [source,java] ---- -DefaultRelyingPartyRegistrationResolver relyingPartyRegistrationResolver = +Converter relyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository); Saml2MetadataFilter filter = new Saml2MetadataFilter( relyingPartyRegistrationResolver, @@ -1040,9 +1035,11 @@ You can change this by calling the `setRequestMatcher` method on the filter: [source,java] ---- -filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/{registrationId}/metadata", "GET")); +filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET")); ---- +ensuring that the `registrationId` hint is at the end of the path. + Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so: [source,java] diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java index 052a4e82cb..10b667847c 100644 --- a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolver.java @@ -42,13 +42,13 @@ import org.springframework.web.util.UriComponentsBuilder; * @since 5.4 */ public final class DefaultRelyingPartyRegistrationResolver - implements Converter, RelyingPartyRegistrationResolver { + implements Converter { private static final char PATH_DELIMITER = '/'; private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; - private final RequestMatcher registrationRequestMatcher = new AntPathRequestMatcher("/**/{registrationId}"); + private final Converter registrationIdResolver = new RegistrationIdResolver(); public DefaultRelyingPartyRegistrationResolver( RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { @@ -56,28 +56,14 @@ public final class DefaultRelyingPartyRegistrationResolver this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; } - /** - * {@inheritDoc} - */ @Override public RelyingPartyRegistration convert(HttpServletRequest request) { - return resolve(request, null); - } - - /** - * {@inheritDoc} - */ - @Override - public RelyingPartyRegistration resolve(HttpServletRequest request, String relyingPartyRegistrationId) { - if (relyingPartyRegistrationId == null) { - relyingPartyRegistrationId = this.registrationRequestMatcher.matcher(request).getVariables() - .get("registrationId"); - } - if (relyingPartyRegistrationId == null) { + String registrationId = this.registrationIdResolver.convert(request); + if (registrationId == null) { return null; } RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationRepository - .findByRegistrationId(relyingPartyRegistrationId); + .findByRegistrationId(registrationId); if (relyingPartyRegistration == null) { return null; } @@ -125,4 +111,16 @@ public final class DefaultRelyingPartyRegistrationResolver return uriComponents.toUriString(); } + private static class RegistrationIdResolver implements Converter { + + private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/**/{registrationId}"); + + @Override + public String convert(HttpServletRequest request) { + RequestMatcher.MatchResult result = this.requestMatcher.matcher(request); + return result.getVariables().get("registrationId"); + } + + } + } diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationResolver.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationResolver.java deleted file mode 100644 index 4a08ab6bdb..0000000000 --- a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationResolver.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2002-2021 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.saml2.provider.service.web; - -import javax.servlet.http.HttpServletRequest; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; - -/** - * A contract for resolving a {@link RelyingPartyRegistration} from the HTTP request - * - * @author Josh Cummings - * @since 5.5 - */ -public interface RelyingPartyRegistrationResolver extends Converter { - - @Override - default RelyingPartyRegistration convert(HttpServletRequest request) { - return resolve(request, null); - } - - /** - * Resolve a {@link RelyingPartyRegistration} from the HTTP request, using the - * {@code relyingPartyRegistrationId}, if it is provided - * @param request the HTTP request - * @param relyingPartyRegistrationId the {@link RelyingPartyRegistration} identifier - * @return the resolved {@link RelyingPartyRegistration} - */ - RelyingPartyRegistration resolve(HttpServletRequest request, String relyingPartyRegistrationId); - -} diff --git a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java index a9f45cd293..57ec493bc8 100644 --- a/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java +++ b/saml2/saml2-service-provider/core/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilter.java @@ -46,7 +46,7 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { public static final String DEFAULT_METADATA_FILE_NAME = "saml-{registrationId}-metadata.xml"; - private final RelyingPartyRegistrationResolver relyingPartyRegistrationResolver; + private final Converter relyingPartyRegistrationConverter; private final Saml2MetadataResolver saml2MetadataResolver; @@ -55,15 +55,11 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { private RequestMatcher requestMatcher = new AntPathRequestMatcher( "/saml2/service-provider-metadata/{registrationId}"); - public Saml2MetadataFilter(Converter relyingPartyRegistrationResolver, + public Saml2MetadataFilter( + Converter relyingPartyRegistrationConverter, Saml2MetadataResolver saml2MetadataResolver) { - if (relyingPartyRegistrationResolver instanceof RelyingPartyRegistrationResolver) { - this.relyingPartyRegistrationResolver = (RelyingPartyRegistrationResolver) relyingPartyRegistrationResolver; - } - else { - this.relyingPartyRegistrationResolver = (request, id) -> relyingPartyRegistrationResolver.convert(request); - } + this.relyingPartyRegistrationConverter = relyingPartyRegistrationConverter; this.saml2MetadataResolver = saml2MetadataResolver; } @@ -75,15 +71,14 @@ public final class Saml2MetadataFilter extends OncePerRequestFilter { chain.doFilter(request, response); return; } - String registrationId = matcher.getVariables().get("registrationId"); - RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationResolver.resolve(request, - registrationId); + RelyingPartyRegistration relyingPartyRegistration = this.relyingPartyRegistrationConverter.convert(request); if (relyingPartyRegistration == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } String metadata = this.saml2MetadataResolver.resolve(relyingPartyRegistration); - writeMetadataToResponse(response, relyingPartyRegistration.getRegistrationId(), metadata); + String registrationId = relyingPartyRegistration.getRegistrationId(); + writeMetadataToResponse(response, registrationId, metadata); } private void writeMetadataToResponse(HttpServletResponse response, String registrationId, String metadata) diff --git a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java index 5970519655..1563951b15 100644 --- a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java +++ b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilterTests.java @@ -22,26 +22,14 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.TestingAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; -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.TestRelyingPartyRegistrations; -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.Saml2AuthenticationTokenConverter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; public class Saml2WebSsoAuthenticationFilterTests { @@ -53,8 +41,6 @@ public class Saml2WebSsoAuthenticationFilterTests { private HttpServletResponse response = new MockHttpServletResponse(); - private AuthenticationManager authenticationManager = mock(AuthenticationManager.class); - @Before public void setup() { this.filter = new Saml2WebSsoAuthenticationFilter(this.repository); @@ -98,26 +84,4 @@ public class Saml2WebSsoAuthenticationFilterTests { .withMessage("No relying party registration found"); } - @Test - public void doFilterWhenPathStartsWithRegistrationIdThenAuthenticates() throws Exception { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); - Authentication authentication = new TestingAuthenticationToken("user", "password"); - given(this.repository.findByRegistrationId("registration-id")).willReturn(registration); - given(this.authenticationManager.authenticate(authentication)).willReturn(authentication); - String loginProcessingUrl = "/{registrationId}/login/saml2/sso"; - RequestMatcher matcher = new AntPathRequestMatcher(loginProcessingUrl); - DefaultRelyingPartyRegistrationResolver delegate = new DefaultRelyingPartyRegistrationResolver(this.repository); - RelyingPartyRegistrationResolver resolver = (request, id) -> { - String registrationId = matcher.matcher(request).getVariables().get("registrationId"); - return delegate.resolve(request, registrationId); - }; - Saml2AuthenticationTokenConverter authenticationConverter = new Saml2AuthenticationTokenConverter(resolver); - this.filter = new Saml2WebSsoAuthenticationFilter(authenticationConverter, loginProcessingUrl); - this.filter.setAuthenticationManager(this.authenticationManager); - this.request.setPathInfo("/registration-id/login/saml2/sso"); - this.request.setParameter("SAMLResponse", "response"); - this.filter.doFilter(this.request, this.response, new MockFilterChain()); - verify(this.repository).findByRegistrationId("registration-id"); - } - } diff --git a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java index 375ad2bc1e..3f0bf6cf84 100644 --- a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java +++ b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java @@ -36,13 +36,7 @@ import org.springframework.security.saml2.provider.service.authentication.TestSa 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.Saml2MessageBinding; -import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; -import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; -import org.springframework.security.saml2.provider.service.web.DefaultSaml2AuthenticationRequestContextResolver; -import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriUtils; @@ -222,29 +216,4 @@ public class Saml2WebSsoAuthenticationRequestFilterTests { assertThat(this.response.getStatus()).isEqualTo(401); } - @Test - public void doFilterWhenPathStartsWithRegistrationIdThenPosts() throws Exception { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full() - .assertingPartyDetails((party) -> party.singleSignOnServiceBinding(Saml2MessageBinding.POST)).build(); - RequestMatcher matcher = new AntPathRequestMatcher("/{registrationId}/saml2/authenticate"); - DefaultRelyingPartyRegistrationResolver delegate = new DefaultRelyingPartyRegistrationResolver(this.repository); - RelyingPartyRegistrationResolver resolver = (request, id) -> { - String registrationId = matcher.matcher(request).getVariables().get("registrationId"); - return delegate.resolve(request, registrationId); - }; - Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver = new DefaultSaml2AuthenticationRequestContextResolver( - resolver); - Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class); - given(authenticationRequest.getAuthenticationRequestUri()).willReturn("uri"); - given(authenticationRequest.getRelayState()).willReturn("relay"); - given(authenticationRequest.getSamlRequest()).willReturn("saml"); - given(this.repository.findByRegistrationId("registration-id")).willReturn(registration); - given(this.factory.createPostAuthenticationRequest(any())).willReturn(authenticationRequest); - this.filter = new Saml2WebSsoAuthenticationRequestFilter(authenticationRequestContextResolver, this.factory); - this.filter.setRedirectMatcher(matcher); - this.request.setPathInfo("/registration-id/saml2/authenticate"); - this.filter.doFilter(this.request, this.response, new MockFilterChain()); - verify(this.repository).findByRegistrationId("registration-id"); - } - } diff --git a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java index 02c7a95cd0..f71bfcea89 100644 --- a/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java +++ b/saml2/saml2-service-provider/core/src/test/java/org/springframework/security/saml2/provider/service/web/Saml2MetadataFilterTests.java @@ -25,7 +25,6 @@ import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.saml2.core.TestSaml2X509Credentials; @@ -38,7 +37,6 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -138,20 +136,6 @@ public class Saml2MetadataFilterTests { .isEqualTo("attachment; filename=\"%s\"; filename*=UTF-8''%s", fileName, encodedFileName); } - @Test - public void doFilterWhenPathStartsWithRegistrationIdThenServesMetadata() throws Exception { - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); - given(this.repository.findByRegistrationId("registration-id")).willReturn(registration); - given(this.resolver.resolve(any())).willReturn("metadata"); - DefaultRelyingPartyRegistrationResolver resolver = new DefaultRelyingPartyRegistrationResolver( - (id) -> this.repository.findByRegistrationId("registration-id")); - this.filter = new Saml2MetadataFilter(resolver, this.resolver); - this.filter.setRequestMatcher(new AntPathRequestMatcher("/metadata")); - this.request.setPathInfo("/metadata"); - this.filter.doFilter(this.request, this.response, new MockFilterChain()); - verify(this.repository).findByRegistrationId("registration-id"); - } - @Test public void setRequestMatcherWhenNullThenIllegalArgument() { assertThatIllegalArgumentException().isThrownBy(() -> this.filter.setRequestMatcher(null));