1
0
mirror of synced 2026-05-22 13:23:17 +00:00

Use original query string to verify signature

Closes gh-11235
This commit is contained in:
Josh Cummings
2022-05-20 17:45:38 -06:00
parent 88f9529329
commit 5cbc1a47da
8 changed files with 181 additions and 70 deletions
@@ -17,9 +17,11 @@
package org.springframework.security.saml2.jackson2;
import java.util.Map;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -46,6 +48,9 @@ import org.springframework.security.saml2.provider.service.registration.Saml2Mes
@JsonIgnoreProperties(ignoreUnknown = true)
class Saml2LogoutRequestMixin {
@JsonIgnore
Function<Map<String, String>, String> encoder;
@JsonCreator
Saml2LogoutRequestMixin(@JsonProperty("location") String location,
@JsonProperty("relayState") Saml2MessageBinding relayState,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -50,7 +50,7 @@ import org.springframework.security.saml2.core.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility methods for verifying SAML component signatures with OpenSAML
@@ -191,8 +191,9 @@ final class OpenSamlVerificationUtils {
else {
this.signature = null;
}
this.content = content(request.getSamlRequest(), Saml2ParameterNames.SAML_REQUEST,
request.getRelayState(), request.getParameter(Saml2ParameterNames.SIG_ALG));
this.content = UriComponentsBuilder.newInstance().query(request.getParametersQuery())
.replaceQueryParam(Saml2ParameterNames.SIGNATURE).build(true).toUriString().substring(1)
.getBytes(StandardCharsets.UTF_8);
}
RedirectSignature(Saml2LogoutResponse response) {
@@ -203,22 +204,9 @@ final class OpenSamlVerificationUtils {
else {
this.signature = null;
}
this.content = content(response.getSamlResponse(), Saml2ParameterNames.SAML_RESPONSE,
response.getRelayState(), response.getParameter(Saml2ParameterNames.SIG_ALG));
}
static byte[] content(String samlObject, String objectParameterName, String relayState, String algorithm) {
if (relayState != null) {
return String.format("%s=%s&%s=%s&%s=%s", objectParameterName,
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1), Saml2ParameterNames.RELAY_STATE,
UriUtils.encode(relayState, StandardCharsets.ISO_8859_1), Saml2ParameterNames.SIG_ALG,
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1)).getBytes(StandardCharsets.UTF_8);
}
else {
return String.format("%s=%s&%s=%s", objectParameterName,
UriUtils.encode(samlObject, StandardCharsets.ISO_8859_1), Saml2ParameterNames.SIG_ALG,
UriUtils.encode(algorithm, StandardCharsets.ISO_8859_1)).getBytes(StandardCharsets.UTF_8);
}
this.content = UriComponentsBuilder.newInstance().query(response.getParametersQuery())
.replaceQueryParam(Saml2ParameterNames.SIGNATURE).build(true).toUriString().substring(1)
.getBytes(StandardCharsets.UTF_8);
}
byte[] getContent() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,15 +17,19 @@
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A class that represents a signed and serialized SAML 2.0 Logout Request
@@ -35,6 +39,17 @@ import org.springframework.security.saml2.provider.service.web.authentication.lo
*/
public final class Saml2LogoutRequest implements Serializable {
private static final Function<Map<String, String>, String> DEFAULT_ENCODER = (params) -> {
if (params.isEmpty()) {
return null;
}
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : params.entrySet()) {
builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
return builder.build(true).toString().substring(1);
};
private final String location;
private final Saml2MessageBinding binding;
@@ -45,13 +60,21 @@ public final class Saml2LogoutRequest implements Serializable {
private final String relyingPartyRegistrationId;
private Function<Map<String, String>, String> encoder;
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
String relyingPartyRegistrationId) {
this(location, binding, parameters, id, relyingPartyRegistrationId, DEFAULT_ENCODER);
}
private Saml2LogoutRequest(String location, Saml2MessageBinding binding, Map<String, String> parameters, String id,
String relyingPartyRegistrationId, Function<Map<String, String>, String> encoder) {
this.location = location;
this.binding = binding;
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
this.id = id;
this.relyingPartyRegistrationId = relyingPartyRegistrationId;
this.encoder = encoder;
}
/**
@@ -119,6 +142,16 @@ public final class Saml2LogoutRequest implements Serializable {
return this.parameters;
}
/**
* Get an encoded query string of all parameters. Resulting query does not contain a
* leading question mark.
* @return an encoded string of all parameters
* @since 5.8
*/
public String getParametersQuery() {
return this.encoder.apply(this.parameters);
}
/**
* The identifier for the {@link RelyingPartyRegistration} associated with this Logout
* Request
@@ -149,7 +182,9 @@ public final class Saml2LogoutRequest implements Serializable {
private Saml2MessageBinding binding;
private Map<String, String> parameters = new HashMap<>();
private Map<String, String> parameters = new LinkedHashMap<>();
private Function<Map<String, String>, String> encoder = DEFAULT_ENCODER;
private String id;
@@ -235,13 +270,28 @@ public final class Saml2LogoutRequest implements Serializable {
return this;
}
/**
* Use this strategy for converting parameters into an encoded query string. The
* resulting query does not contain a leading question mark.
*
* In the event that you already have an encoded version that you want to use, you
* can call this by doing {@code parameterEncoder((params) -> encodedValue)}.
* @param encoder the strategy to use
* @return the {@link Builder} for further configurations
* @since 5.8
*/
public Builder parametersQuery(Function<Map<String, String>, String> encoder) {
this.encoder = encoder;
return this;
}
/**
* Build the {@link Saml2LogoutRequest}
* @return a constructed {@link Saml2LogoutRequest}
*/
public Saml2LogoutRequest build() {
return new Saml2LogoutRequest(this.location, this.binding, this.parameters, this.id,
this.registration.getRegistrationId());
this.registration.getRegistrationId(), this.encoder);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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,15 +16,19 @@
package org.springframework.security.saml2.provider.service.authentication.logout;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* A class that represents a signed and serialized SAML 2.0 Logout Response
@@ -34,16 +38,31 @@ import org.springframework.security.saml2.provider.service.web.authentication.lo
*/
public final class Saml2LogoutResponse {
private static final Function<Map<String, String>, String> DEFAULT_ENCODER = (params) -> {
if (params.isEmpty()) {
return null;
}
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
for (Map.Entry<String, String> component : params.entrySet()) {
builder.queryParam(component.getKey(), UriUtils.encode(component.getValue(), StandardCharsets.ISO_8859_1));
}
return builder.build(true).toString().substring(1);
};
private final String location;
private final Saml2MessageBinding binding;
private final Map<String, String> parameters;
private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map<String, String> parameters) {
private final Function<Map<String, String>, String> encoder;
private Saml2LogoutResponse(String location, Saml2MessageBinding binding, Map<String, String> parameters,
Function<Map<String, String>, String> encoder) {
this.location = location;
this.binding = binding;
this.parameters = Collections.unmodifiableMap(new HashMap<>(parameters));
this.parameters = Collections.unmodifiableMap(new LinkedHashMap<>(parameters));
this.encoder = encoder;
}
/**
@@ -103,6 +122,16 @@ public final class Saml2LogoutResponse {
return this.parameters;
}
/**
* Get an encoded query string of all parameters. Resulting query does not contain a
* leading question mark.
* @return an encoded string of all parameters
* @since 5.8
*/
public String getParametersQuery() {
return this.encoder.apply(this.parameters);
}
/**
* Create a {@link Builder} instance from this {@link RelyingPartyRegistration}
*
@@ -122,7 +151,9 @@ public final class Saml2LogoutResponse {
private Saml2MessageBinding binding;
private Map<String, String> parameters = new HashMap<>();
private Map<String, String> parameters = new LinkedHashMap<>();
private Function<Map<String, String>, String> encoder = DEFAULT_ENCODER;
private Builder(RelyingPartyRegistration registration) {
this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation();
@@ -195,12 +226,27 @@ public final class Saml2LogoutResponse {
return this;
}
/**
* Use this strategy for converting parameters into an encoded query string. The
* resulting query does not contain a leading question mark.
*
* In the event that you already have an encoded version that you want to use, you
* can call this by doing {@code parameterEncoder((params) -> encodedValue)}.
* @param encoder the strategy to use
* @return the {@link Saml2LogoutRequest.Builder} for further configurations
* @since 5.8
*/
public Builder parametersQuery(Function<Map<String, String>, String> encoder) {
this.encoder = encoder;
return this;
}
/**
* Build the {@link Saml2LogoutResponse}
* @return a constructed {@link Saml2LogoutResponse}
*/
public Saml2LogoutResponse build() {
return new Saml2LogoutResponse(this.location, this.binding, this.parameters);
return new Saml2LogoutResponse(this.location, this.binding, this.parameters, this.encoder);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,8 +17,6 @@
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -52,7 +50,6 @@ 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
@@ -140,7 +137,7 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
request.getParameter(Saml2ParameterNames.SIG_ALG)))
.parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE,
request.getParameter(Saml2ParameterNames.SIGNATURE)))
.build();
.parametersQuery((params) -> request.getQueryString()).build();
Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(logoutRequest,
registration, authentication);
Saml2LogoutValidatorResult result = this.logoutRequestValidator.validate(parameters);
@@ -191,22 +188,11 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
private void doRedirect(HttpServletRequest request, HttpServletResponse response,
Saml2LogoutResponse logoutResponse) throws IOException {
String location = logoutResponse.getResponseLocation();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter(Saml2ParameterNames.SAML_RESPONSE, logoutResponse::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, logoutResponse::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.SIG_ALG, logoutResponse::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.SIGNATURE, logoutResponse::getParameter, uriBuilder);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location)
.query(logoutResponse.getParametersQuery());
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(parameters.apply(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutResponse logoutResponse) throws IOException {
String location = logoutResponse.getResponseLocation();
String saml = logoutResponse.getSamlResponse();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -140,7 +140,7 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter {
request.getParameter(Saml2ParameterNames.SIG_ALG)))
.parameters((params) -> params.put(Saml2ParameterNames.SIGNATURE,
request.getParameter(Saml2ParameterNames.SIGNATURE)))
.build();
.parametersQuery((params) -> request.getQueryString()).build();
Saml2LogoutResponseValidatorParameters parameters = new Saml2LogoutResponseValidatorParameters(logoutResponse,
logoutRequest, registration);
Saml2LogoutValidatorResult result = this.logoutResponseValidator.validate(parameters);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -17,8 +17,6 @@
package org.springframework.security.saml2.provider.service.web.authentication.logout;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -27,7 +25,6 @@ import org.apache.commons.logging.LogFactory;
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;
@@ -37,7 +34,6 @@ 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 the SAML 2.0 Asserting
@@ -104,22 +100,11 @@ public final class Saml2RelyingPartyInitiatedLogoutSuccessHandler implements Log
private void doRedirect(HttpServletRequest request, HttpServletResponse response, Saml2LogoutRequest logoutRequest)
throws IOException {
String location = logoutRequest.getLocation();
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location);
addParameter(Saml2ParameterNames.SAML_REQUEST, logoutRequest::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.RELAY_STATE, logoutRequest::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.SIG_ALG, logoutRequest::getParameter, uriBuilder);
addParameter(Saml2ParameterNames.SIGNATURE, logoutRequest::getParameter, uriBuilder);
UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(location)
.query(logoutRequest.getParametersQuery());
this.redirectStrategy.sendRedirect(request, response, uriBuilder.build(true).toUriString());
}
private void addParameter(String name, Function<String, String> parameters, UriComponentsBuilder builder) {
Assert.hasText(name, "name cannot be empty or null");
if (StringUtils.hasText(parameters.apply(name))) {
builder.queryParam(UriUtils.encode(name, StandardCharsets.ISO_8859_1),
UriUtils.encode(parameters.apply(name), StandardCharsets.ISO_8859_1));
}
}
private void doPost(HttpServletResponse response, Saml2LogoutRequest logoutRequest) throws IOException {
String location = logoutRequest.getLocation();
String saml = logoutRequest.getSamlRequest();