diff --git a/docs/modules/ROOT/pages/migration-7/saml2.adoc b/docs/modules/ROOT/pages/migration-7/saml2.adoc new file mode 100644 index 0000000000..9a8cb60080 --- /dev/null +++ b/docs/modules/ROOT/pages/migration-7/saml2.adoc @@ -0,0 +1,60 @@ += Saml 2.0 Migrations + +== Continue Filter Chain When No Relying Party Found + +In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found. + +There are a number of cases when an application would not consider this an error situation. +For example, this filter doesn't know how the `AuthorizationFilter` will respond to a missing relying party. +In some cases it may be allowable. + +In other cases, you may want your `AuthenticationEntryPoint` to be invoked, which would happen if this filter were to allow the request to continue to the `AuthorizationFilter`. + +To improve this filter's flexibility, in Spring Security 7 it will continue the filter chain when there is no relying party registration found instead of throwing an exception. + +For many applications, the only notable change will be that your `authenticationEntryPoint` will be invoked if the relying party registration cannot be found. +When you have only one asserting party, this means by default a new authentication request will be built and sent back to the asserting party, which may cause a "Too Many Redirects" loop. + +To see if you are affected in this way, you can prepare for this change in 6 by setting the following property in `Saml2WebSsoAuthenticationFilter`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .saml2Login((saml2) -> saml2 + .withObjectPostProcessor(new ObjectPostProcessor() { + @Override + public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) { + filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true); + return filter; + } + }) + ) +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Login { } + withObjectPostProcessor( + object : ObjectPostProcessor() { + override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter { + filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true) + return filter + } + }) +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + +---- +====== diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java index 6c9ed2dc13..9c584a7501 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java @@ -54,6 +54,8 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce private Saml2AuthenticationRequestRepository authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository(); + private boolean continueChainWhenNoRelyingPartyRegistrationFound = false; + /** * Creates a {@code Saml2WebSsoAuthenticationFilter} authentication filter that is * configured to use the {@link #DEFAULT_FILTER_PROCESSES_URI} processing URL @@ -94,6 +96,7 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce this.authenticationConverter = authenticationConverter; setAllowSessionCreation(true); setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy()); + setAuthenticationConverter(authenticationConverter); } /** @@ -110,6 +113,7 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce this.authenticationConverter = authenticationConverter; setAllowSessionCreation(true); setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy()); + setAuthenticationConverter(authenticationConverter); } @Override @@ -122,6 +126,9 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce throws AuthenticationException { Authentication authentication = this.authenticationConverter.convert(request); if (authentication == null) { + if (this.continueChainWhenNoRelyingPartyRegistrationFound) { + return null; + } Saml2Error saml2Error = new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND, "No relying party registration found"); throw new Saml2AuthenticationException(saml2Error); @@ -156,10 +163,24 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce } private void setDetails(HttpServletRequest request, Authentication authentication) { + if (authentication.getDetails() != null) { + return; + } if (authentication instanceof AbstractAuthenticationToken token) { Object details = this.authenticationDetailsSource.buildDetails(request); token.setDetails(details); } } + /** + * Indicate whether to continue with the rest of the filter chain in the event that no + * relying party registration is found. This is {@code false} by default, meaning that + * it will throw an exception. + * @param continueChain whether to continue + * @since 6.5 + */ + public void setContinueChainWhenNoRelyingPartyRegistrationFound(boolean continueChain) { + this.continueChainWhenNoRelyingPartyRegistrationFound = continueChain; + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java index 3ef96f8481..9e4b4d3269 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,7 @@ package org.springframework.security.saml2.provider.service.web.authentication; +import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; @@ -121,6 +122,31 @@ public class Saml2WebSsoAuthenticationFilterTests { .withMessage("No relying party registration found"); } + @Test + public void doFilterWhenContinueChainRegistrationIdDoesNotExistThenContinues() throws Exception { + given(this.repository.findByRegistrationId("non-existent-id")).willReturn(null); + this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}"); + this.filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true); + this.request.setRequestURI("/some/other/path/non-existent-id"); + this.request.setPathInfo("/some/other/path/non-existent-id"); + FilterChain chain = mock(FilterChain.class); + this.filter.doFilter(this.request, this.response, chain); + verify(chain).doFilter(this.request, this.response); + } + + @Test + public void doFilterWhenContinueChainNoSamlResponseThenContinues() throws Exception { + given(this.repository.findByRegistrationId("id")).willReturn(TestRelyingPartyRegistrations.full().build()); + this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}"); + this.filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true); + this.request.setRequestURI("/some/other/path/id"); + this.request.setPathInfo("/some/other/path/id"); + this.request.removeParameter(Saml2ParameterNames.SAML_RESPONSE); + FilterChain chain = mock(FilterChain.class); + this.filter.doFilter(this.request, this.response, chain); + verify(chain).doFilter(this.request, this.response); + } + @Test public void attemptAuthenticationWhenSavedAuthnRequestThenRemovesAuthnRequest() { Saml2AuthenticationRequestRepository authenticationRequestRepository = mock(