Browse Source

Support Continue Filter Chain When No Relying Party

Closes gh-16000
Josh Cummings 4 months ago
parent
commit
67c21de1cf

+ 60 - 0
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<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"/>
+----
+======

+ 21 - 0
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<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;
+	}
+
 }

+ 27 - 1
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<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = mock(