1
0
mirror of synced 2026-07-07 03:40:04 +00:00

Merge branch 7.0.x

This commit is contained in:
Josh Cummings
2026-05-29 14:43:34 -06:00
6 changed files with 58 additions and 164 deletions
@@ -25,17 +25,17 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver;
import org.springframework.security.web.FormPostRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
@@ -68,6 +68,8 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
private Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository = new HttpSessionSaml2AuthenticationRequestRepository();
private final RedirectStrategy formPostRedirectStrategy = new FormPostRedirectStrategy();
/**
* Construct a {@link Saml2WebSsoAuthenticationRequestFilter} with the strategy for
* resolving the {@code AuthnRequest}
@@ -133,54 +135,11 @@ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter
private void sendPost(HttpServletRequest request, HttpServletResponse response,
Saml2PostAuthenticationRequest authenticationRequest) throws IOException {
this.authenticationRequestRepository.saveAuthenticationRequest(authenticationRequest, request, response);
String html = createSamlPostRequestFormData(authenticationRequest);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
}
private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest authenticationRequest) {
String authenticationRequestUri = authenticationRequest.getAuthenticationRequestUri();
String relayState = authenticationRequest.getRelayState();
String samlRequest = authenticationRequest.getSamlRequest();
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta http-equiv=\"Content-Security-Policy\" ")
.append("content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body>\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(authenticationRequestUri);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLRequest\" value=\"");
html.append(HtmlUtils.htmlEscape(samlRequest));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" <script>window.onload = function() { document.forms[0].submit(); }</script>\n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authenticationRequest.getAuthenticationRequestUri());
addParameter(Saml2ParameterNames.SAML_REQUEST, authenticationRequest.getSamlRequest(), uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, authenticationRequest.getRelayState(), uriBuilder);
this.formPostRedirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
}
@@ -17,6 +17,7 @@
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import jakarta.servlet.FilterChain;
@@ -28,7 +29,6 @@ import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -49,6 +49,7 @@ import org.springframework.security.saml2.provider.service.web.RelyingPartyRegis
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationPlaceholderResolvers.UriResolver;
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.FormPostRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
@@ -57,8 +58,8 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A filter for handling logout requests in the form of a &lt;saml2:LogoutRequest&gt; sent
@@ -85,6 +86,8 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final RedirectStrategy formPostRedirectStrategy = new FormPostRedirectStrategy();
public Saml2LogoutRequestFilter(Saml2LogoutRequestValidatorParametersResolver logoutRequestResolver,
Saml2LogoutRequestValidator logoutRequestValidator, Saml2LogoutResponseResolver logoutResponseResolver,
LogoutHandler... handlers) {
@@ -205,7 +208,7 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
doRedirect(request, response, logoutResponse);
}
else {
doPost(response, logoutResponse);
doPost(request, response, logoutResponse);
}
}
@@ -218,55 +221,21 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException {
private void doPost(HttpServletRequest request, HttpServletResponse response, Saml2LogoutResponse logoutResponse)
throws IOException {
String location = logoutResponse.getResponseLocation();
String saml = logoutResponse.getSamlResponse();
String relayState = logoutResponse.getRelayState();
String html = createSamlPostRequestFormData(location, saml, relayState);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter(Saml2ParameterNames.SAML_RESPONSE, logoutResponse.getSamlResponse(), uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, logoutResponse.getRelayState(), uriBuilder);
this.formPostRedirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private String createSamlPostRequestFormData(String location, String saml, @Nullable String relayState) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta http-equiv=\"Content-Security-Policy\" ")
.append("content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body>\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(location);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLResponse\" value=\"");
html.append(HtmlUtils.htmlEscape(saml));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
private void addParameter(String name, @Nullable String value, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(value)) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(value, StandardCharsets.ISO_8859_1));
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" <script>window.onload = function() { document.forms[0].submit(); }</script>\n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
}
private static class Saml2AssertingPartyLogoutRequestResolver
@@ -17,6 +17,7 @@
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -24,17 +25,18 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.FormPostRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A success handler for issuing a SAML 2.0 Logout Request to the SAML 2.0 Asserting Party
@@ -50,6 +52,8 @@ public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements Log
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final RedirectStrategy formPostRedirectStrategy = new FormPostRedirectStrategy();
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
/**
@@ -88,7 +92,7 @@ public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements Log
doRedirect(request, response, logoutRequest);
}
else {
doPost(response, logoutRequest);
doPost(request, response, logoutRequest);
}
}
@@ -111,55 +115,21 @@ public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements Log
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException {
private void doPost(HttpServletRequest request, HttpServletResponse response, Saml2LogoutRequest logoutRequest)
throws IOException {
String location = logoutRequest.getLocation();
String saml = logoutRequest.getSamlRequest();
String relayState = logoutRequest.getRelayState();
String html = createSamlPostRequestFormData(location, saml, relayState);
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(html);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter(Saml2ParameterNames.SAML_REQUEST, logoutRequest.getSamlRequest(), uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, logoutRequest.getRelayState(), uriBuilder);
this.formPostRedirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private String createSamlPostRequestFormData(String location, String saml, @Nullable String relayState) {
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n").append(" <head>\n");
html.append(" <meta http-equiv=\"Content-Security-Policy\" ")
.append("content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">\n");
html.append(" <meta charset=\"utf-8\" />\n");
html.append(" </head>\n");
html.append(" <body>\n");
html.append(" <noscript>\n");
html.append(" <p>\n");
html.append(" <strong>Note:</strong> Since your browser does not support JavaScript,\n");
html.append(" you must press the Continue button once to proceed.\n");
html.append(" </p>\n");
html.append(" </noscript>\n");
html.append(" \n");
html.append(" <form action=\"");
html.append(location);
html.append("\" method=\"post\">\n");
html.append(" <div>\n");
html.append(" <input type=\"hidden\" name=\"SAMLRequest\" value=\"");
html.append(HtmlUtils.htmlEscape(saml));
html.append("\"/>\n");
if (StringUtils.hasText(relayState)) {
html.append(" <input type=\"hidden\" name=\"RelayState\" value=\"");
html.append(HtmlUtils.htmlEscape(relayState));
html.append("\"/>\n");
private void addParameter(String name, @Nullable String value, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(value)) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(value, StandardCharsets.ISO_8859_1));
}
html.append(" </div>\n");
html.append(" <noscript>\n");
html.append(" <div>\n");
html.append(" <input type=\"submit\" value=\"Continue\"/>\n");
html.append(" </div>\n");
html.append(" </noscript>\n");
html.append(" </form>\n");
html.append(" \n");
html.append(" <script>window.onload = function() { document.forms[0].submit(); }</script>\n");
html.append(" </body>\n");
html.append("</html>");
return html.toString();
}
}
@@ -178,12 +178,10 @@ public class Saml2WebSsoAuthenticationRequestFilterTests {
given(this.authenticationRequestResolver.resolve(any())).willReturn(request);
this.filter.doFilterInternal(this.request, this.response, this.filterChain);
assertThat(this.response.getHeader("Location")).isNull();
assertThat(this.response.getContentAsString()).contains(
"<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">")
.contains("<script>window.onload = function() { document.forms[0].submit(); }</script>")
.contains("<form action=\"https://sso-url.example.com/IDP/SSO\" method=\"post\">")
.contains("<input type=\"hidden\" name=\"SAMLRequest\"")
assertThat(this.response.getContentAsString()).contains("action=\"https://sso-url.example.com/IDP/SSO\"")
.contains("name=\"SAMLRequest\"")
.contains("value=\"" + relayStateEncoded + "\"");
assertThat(this.response.getHeader("Content-Security-Policy")).matches("script-src 'nonce-.+'");
}
@Test
@@ -118,7 +118,7 @@ public class Saml2LogoutRequestFilterTests {
verify(this.logoutRequestValidator).validate(any());
verify(this.logoutHandler).logout(any(), any(), any());
verify(this.logoutResponseResolver).resolve(any(), any());
checkResponse(response.getContentAsString(), registration);
checkResponse(response, registration);
verify(this.securityContextHolderStrategy).getContext();
}
@@ -165,7 +165,7 @@ public class Saml2LogoutRequestFilterTests {
this.logoutRequestProcessingFilter.doFilter(request, response, new MockFilterChain());
checkResponse(response.getContentAsString(), registration);
checkResponse(response, registration);
verify(this.logoutRequestValidator).validate(any());
verify(this.logoutResponseResolver).resolve(any(), any(),
argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_REQUEST)));
@@ -194,7 +194,7 @@ public class Saml2LogoutRequestFilterTests {
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
checkResponse(response.getContentAsString(), registration);
checkResponse(response, registration);
verify(this.logoutResponseResolver).resolve(any(), any(),
argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_DESTINATION)));
verifyNoInteractions(this.logoutHandler);
@@ -225,7 +225,7 @@ public class Saml2LogoutRequestFilterTests {
this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain());
checkResponse(response.getContentAsString(), registration);
checkResponse(response, registration);
verify(this.logoutResponseResolver).resolve(any(), any(),
argThat((ex) -> ex.getSaml2Error().getErrorCode().equals(Saml2ErrorCodes.INVALID_REQUEST)));
verifyNoInteractions(this.logoutHandler);
@@ -259,15 +259,14 @@ public class Saml2LogoutRequestFilterTests {
verifyNoInteractions(this.logoutHandler);
}
private void checkResponse(String responseContent, RelyingPartyRegistration registration) {
private void checkResponse(MockHttpServletResponse response, RelyingPartyRegistration registration)
throws Exception {
String responseContent = response.getContentAsString();
assertThat(responseContent).contains(Saml2ParameterNames.SAML_RESPONSE);
assertThat(responseContent)
.contains(registration.getAssertingPartyMetadata().getSingleLogoutServiceResponseLocation());
assertThat(responseContent).contains(
"<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">");
assertThat(responseContent)
.contains("<script>window.onload = function() { document.forms[0].submit(); }</script>");
assertThat(response.getHeader("Content-Security-Policy")).matches("script-src 'nonce-.+'");
assertThat(responseContent).contains("document.getElementById(\"redirect-form\").submit();");
}
}
@@ -99,9 +99,8 @@ public class Saml2RelyingPartyInitiatedLogoutSuccessHandlerTests {
String content = response.getContentAsString();
assertThat(content).contains(Saml2ParameterNames.SAML_REQUEST);
assertThat(content).contains(registration.getAssertingPartyMetadata().getSingleLogoutServiceLocation());
assertThat(content).contains(
"<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'sha256-oZhLbc2kO8b8oaYLrUc7uye1MgVKMyLtPqWR4WtKF+c='\">");
assertThat(content).contains("<script>window.onload = function() { document.forms[0].submit(); }</script>");
assertThat(response.getHeader("Content-Security-Policy")).matches("script-src 'nonce-.+'");
assertThat(content).contains("document.getElementById(\"redirect-form\").submit();");
}
private Saml2Authentication authentication(RelyingPartyRegistration registration) {