Support Continue Filter Chain When No Relying Party

Closes gh-16000
This commit is contained in:
Josh Cummings 2025-04-03 15:32:04 -06:00
parent 5436fd5574
commit 67c21de1cf
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
3 changed files with 108 additions and 1 deletions

View File

@ -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<Saml2WebSsoAuhenticaionFilter>() {
@Override
public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
return filter;
}
})
)
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
http {
saml2Login { }
withObjectPostProcessor(
object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
return filter
}
})
}
----
Xml::
+
[source,xml,role="secondary"]
----
<b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/>
----
======

View File

@ -54,6 +54,8 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
private Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> 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;
}
}

View File

@ -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<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = mock(