Skip to content

Commit 67c21de

Browse files
committed
Support Continue Filter Chain When No Relying Party
Closes gh-16000
1 parent 5436fd5 commit 67c21de

File tree

3 files changed

+108
-1
lines changed

3 files changed

+108
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
= Saml 2.0 Migrations
2+
3+
== Continue Filter Chain When No Relying Party Found
4+
5+
In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found.
6+
7+
There are a number of cases when an application would not consider this an error situation.
8+
For example, this filter doesn't know how the `AuthorizationFilter` will respond to a missing relying party.
9+
In some cases it may be allowable.
10+
11+
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`.
12+
13+
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.
14+
15+
For many applications, the only notable change will be that your `authenticationEntryPoint` will be invoked if the relying party registration cannot be found.
16+
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.
17+
18+
To see if you are affected in this way, you can prepare for this change in 6 by setting the following property in `Saml2WebSsoAuthenticationFilter`:
19+
20+
[tabs]
21+
======
22+
Java::
23+
+
24+
[source,java,role="primary"]
25+
----
26+
http
27+
.saml2Login((saml2) -> saml2
28+
.withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() {
29+
@Override
30+
public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
31+
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
32+
return filter;
33+
}
34+
})
35+
)
36+
----
37+
38+
Kotlin::
39+
+
40+
[source,kotlin,role="secondary"]
41+
----
42+
http {
43+
saml2Login { }
44+
withObjectPostProcessor(
45+
object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
46+
override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
47+
filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
48+
return filter
49+
}
50+
})
51+
}
52+
----
53+
54+
Xml::
55+
+
56+
[source,xml,role="secondary"]
57+
----
58+
<b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/>
59+
----
60+
======

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilter.java

+21
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public class Saml2WebSsoAuthenticationFilter extends AbstractAuthenticationProce
5454

5555
private Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository();
5656

57+
private boolean continueChainWhenNoRelyingPartyRegistrationFound = false;
58+
5759
/**
5860
* Creates a {@code Saml2WebSsoAuthenticationFilter} authentication filter that is
5961
* configured to use the {@link #DEFAULT_FILTER_PROCESSES_URI} processing URL
@@ -94,6 +96,7 @@ public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationCon
9496
this.authenticationConverter = authenticationConverter;
9597
setAllowSessionCreation(true);
9698
setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy());
99+
setAuthenticationConverter(authenticationConverter);
97100
}
98101

99102
/**
@@ -110,6 +113,7 @@ public Saml2WebSsoAuthenticationFilter(AuthenticationConverter authenticationCon
110113
this.authenticationConverter = authenticationConverter;
111114
setAllowSessionCreation(true);
112115
setSessionAuthenticationStrategy(new ChangeSessionIdAuthenticationStrategy());
116+
setAuthenticationConverter(authenticationConverter);
113117
}
114118

115119
@Override
@@ -122,6 +126,9 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
122126
throws AuthenticationException {
123127
Authentication authentication = this.authenticationConverter.convert(request);
124128
if (authentication == null) {
129+
if (this.continueChainWhenNoRelyingPartyRegistrationFound) {
130+
return null;
131+
}
125132
Saml2Error saml2Error = new Saml2Error(Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND,
126133
"No relying party registration found");
127134
throw new Saml2AuthenticationException(saml2Error);
@@ -156,10 +163,24 @@ private void setAuthenticationRequestRepositoryIntoAuthenticationConverter(
156163
}
157164

158165
private void setDetails(HttpServletRequest request, Authentication authentication) {
166+
if (authentication.getDetails() != null) {
167+
return;
168+
}
159169
if (authentication instanceof AbstractAuthenticationToken token) {
160170
Object details = this.authenticationDetailsSource.buildDetails(request);
161171
token.setDetails(details);
162172
}
163173
}
164174

175+
/**
176+
* Indicate whether to continue with the rest of the filter chain in the event that no
177+
* relying party registration is found. This is {@code false} by default, meaning that
178+
* it will throw an exception.
179+
* @param continueChain whether to continue
180+
* @since 6.5
181+
*/
182+
public void setContinueChainWhenNoRelyingPartyRegistrationFound(boolean continueChain) {
183+
this.continueChainWhenNoRelyingPartyRegistrationFound = continueChain;
184+
}
185+
165186
}

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2WebSsoAuthenticationFilterTests.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.saml2.provider.service.web.authentication;
1818

19+
import jakarta.servlet.FilterChain;
1920
import jakarta.servlet.http.HttpServletRequest;
2021
import jakarta.servlet.http.HttpServletResponse;
2122
import org.junit.jupiter.api.BeforeEach;
@@ -121,6 +122,31 @@ public void attemptAuthenticationWhenRegistrationIdDoesNotExistThenThrowsExcepti
121122
.withMessage("No relying party registration found");
122123
}
123124

125+
@Test
126+
public void doFilterWhenContinueChainRegistrationIdDoesNotExistThenContinues() throws Exception {
127+
given(this.repository.findByRegistrationId("non-existent-id")).willReturn(null);
128+
this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}");
129+
this.filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
130+
this.request.setRequestURI("/some/other/path/non-existent-id");
131+
this.request.setPathInfo("/some/other/path/non-existent-id");
132+
FilterChain chain = mock(FilterChain.class);
133+
this.filter.doFilter(this.request, this.response, chain);
134+
verify(chain).doFilter(this.request, this.response);
135+
}
136+
137+
@Test
138+
public void doFilterWhenContinueChainNoSamlResponseThenContinues() throws Exception {
139+
given(this.repository.findByRegistrationId("id")).willReturn(TestRelyingPartyRegistrations.full().build());
140+
this.filter = new Saml2WebSsoAuthenticationFilter(this.repository, "/some/other/path/{registrationId}");
141+
this.filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
142+
this.request.setRequestURI("/some/other/path/id");
143+
this.request.setPathInfo("/some/other/path/id");
144+
this.request.removeParameter(Saml2ParameterNames.SAML_RESPONSE);
145+
FilterChain chain = mock(FilterChain.class);
146+
this.filter.doFilter(this.request, this.response, chain);
147+
verify(chain).doFilter(this.request, this.response);
148+
}
149+
124150
@Test
125151
public void attemptAuthenticationWhenSavedAuthnRequestThenRemovesAuthnRequest() {
126152
Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = mock(

0 commit comments

Comments
 (0)