Prechádzať zdrojové kódy

Add ResponseValidator

Issue gh-14264
Closes gh-16915
Josh Cummings 4 mesiacov pred
rodič
commit
3e686abf50

+ 24 - 0
docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc

@@ -359,6 +359,30 @@ provider.setResponseValidator((responseToken) -> {
 });
 ----
 
+When using `OpenSaml5AuthenticationProvider`, you can do the same with less boilerplate:
+
+[source,java]
+----
+OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
+provider.setResponseValidator(responseValidator);
+----
+
+You can also customize which validation steps Spring Security should do.
+For example, if you want to skip `Response#InResponseTo` validation, you can call ``ResponseValidator``'s constructor, excluding `InResponseToValidator` from the list:
+
+[source,java]
+----
+OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
+provider.setResponseValidator(responseValidator);
+----
+
+[TIP]
+====
+OpenSAML performs `Asssertion#InResponseTo` validation in its `BearerSubjectConfirmationValidator` class, which is configurable using <<_performing_additional_assertion_validation, setAssertionValidator>>.
+====
+
 == Performing Additional Assertion Validation
 `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
 After verifying the signature, it will:

+ 3 - 3
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/BaseOpenSamlAuthenticationProvider.java

@@ -183,7 +183,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
 		};
 	}
 
-	private static List<String> getStatusCodes(Response response) {
+	static List<String> getStatusCodes(Response response) {
 		if (response.getStatus() == null) {
 			return List.of(StatusCode.SUCCESS);
 		}
@@ -206,7 +206,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
 		return List.of(parentStatusCodeValue, childStatusCodeValue);
 	}
 
-	private static boolean isSuccess(List<String> statusCodes) {
+	static boolean isSuccess(List<String> statusCodes) {
 		if (statusCodes.size() != 1) {
 			return false;
 		}
@@ -215,7 +215,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
 		return StatusCode.SUCCESS.equals(statusCode);
 	}
 
-	private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
+	static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
 			String inResponseTo) {
 		if (!StringUtils.hasText(inResponseTo)) {
 			return Saml2ResponseValidatorResult.success();

+ 135 - 4
saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java

@@ -53,6 +53,7 @@ import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
 import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
 
 import org.springframework.core.convert.converter.Converter;
+import org.springframework.lang.NonNull;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.core.Authentication;
@@ -60,6 +61,7 @@ import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.saml2.core.Saml2Error;
 import org.springframework.security.saml2.core.Saml2ErrorCodes;
 import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
+import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
@@ -114,6 +116,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 */
 	public OpenSaml5AuthenticationProvider() {
 		this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
+		setResponseValidator(ResponseValidator.withDefaults());
 		setAssertionValidator(AssertionValidator.withDefaults());
 	}
 
@@ -301,12 +304,11 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 * Construct a default strategy for validating the SAML 2.0 Response
 	 * @return the default response validator strategy
 	 * @since 5.6
+	 * @deprecated please use {@link ResponseValidator#withDefaults()} instead
 	 */
+	@Deprecated
 	public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
-		Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
-			.createDefaultResponseValidator();
-		return (token) -> delegate
-			.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
+		return ResponseValidator.withDefaults();
 	}
 
 	/**
@@ -459,6 +461,135 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 
 	}
 
+	/**
+	 * A response validator that checks the {@code InResponseTo} value against the
+	 * correlating {@link AbstractSaml2AuthenticationRequest}
+	 *
+	 * @since 6.5
+	 */
+	public static final class InResponseToValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
+
+		@Override
+		@NonNull
+		public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
+			AbstractSaml2AuthenticationRequest request = responseToken.getToken().getAuthenticationRequest();
+			Response response = responseToken.getResponse();
+			String inResponseTo = response.getInResponseTo();
+			return BaseOpenSamlAuthenticationProvider.validateInResponseTo(request, inResponseTo);
+		}
+
+	}
+
+	/**
+	 * A response validator that compares the {@code Destination} value to the configured
+	 * {@link RelyingPartyRegistration#getAssertionConsumerServiceLocation()}
+	 *
+	 * @since 6.5
+	 */
+	public static final class DestinationValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
+
+		@Override
+		@NonNull
+		public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
+			Response response = responseToken.getResponse();
+			Saml2AuthenticationToken token = responseToken.getToken();
+			String destination = response.getDestination();
+			String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
+			if (StringUtils.hasText(destination) && !destination.equals(location)) {
+				String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID()
+						+ "]";
+				return Saml2ResponseValidatorResult
+					.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message));
+			}
+			return Saml2ResponseValidatorResult.success();
+		}
+
+	}
+
+	/**
+	 * A response validator that compares the {@code Issuer} value to the configured
+	 * {@link AssertingPartyMetadata#getEntityId()}
+	 *
+	 * @since 6.5
+	 */
+	public static final class IssuerValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
+
+		@Override
+		@NonNull
+		public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
+			Response response = responseToken.getResponse();
+			Saml2AuthenticationToken token = responseToken.getToken();
+			String issuer = response.getIssuer().getValue();
+			String assertingPartyEntityId = token.getRelyingPartyRegistration()
+				.getAssertingPartyMetadata()
+				.getEntityId();
+			if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) {
+				String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
+				return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message));
+			}
+			return Saml2ResponseValidatorResult.success();
+		}
+
+	}
+
+	/**
+	 * A composite response validator that confirms a {@code SUCCESS} status, that there
+	 * is at least one assertion, and any other configured converters
+	 *
+	 * @since 6.5
+	 * @see InResponseToValidator
+	 * @see DestinationValidator
+	 * @see IssuerValidator
+	 */
+	public static final class ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
+
+		private static final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> DEFAULTS = List
+			.of(new InResponseToValidator(), new DestinationValidator(), new IssuerValidator());
+
+		private final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> validators;
+
+		@SafeVarargs
+		public ResponseValidator(Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) {
+			this.validators = List.of(validators);
+			Assert.notEmpty(this.validators, "validators cannot be empty");
+		}
+
+		public static ResponseValidator withDefaults() {
+			return new ResponseValidator(new InResponseToValidator(), new DestinationValidator(),
+					new IssuerValidator());
+		}
+
+		@SafeVarargs
+		public static ResponseValidator withDefaults(
+				Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) {
+			List<Converter<ResponseToken, Saml2ResponseValidatorResult>> defaults = new ArrayList<>(DEFAULTS);
+			defaults.addAll(List.of(validators));
+			return new ResponseValidator(defaults.toArray(Converter[]::new));
+		}
+
+		@Override
+		public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
+			Response response = responseToken.getResponse();
+			Collection<Saml2Error> errors = new ArrayList<>();
+			List<String> statusCodes = BaseOpenSamlAuthenticationProvider.getStatusCodes(response);
+			if (!BaseOpenSamlAuthenticationProvider.isSuccess(statusCodes)) {
+				for (String statusCode : statusCodes) {
+					String message = String.format("Invalid status [%s] for SAML response [%s]", statusCode,
+							response.getID());
+					errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message));
+				}
+			}
+			for (Converter<ResponseToken, Saml2ResponseValidatorResult> validator : this.validators) {
+				errors.addAll(validator.convert(responseToken).getErrors());
+			}
+			if (response.getAssertions().isEmpty()) {
+				errors.add(new Saml2Error(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response."));
+			}
+			return Saml2ResponseValidatorResult.failure(errors);
+		}
+
+	}
+
 	/**
 	 * A default implementation of {@link OpenSaml5AuthenticationProvider}'s assertion
 	 * validator. This does not check the signature as signature verification is performed

+ 17 - 0
saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java

@@ -78,6 +78,7 @@ import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
 import org.springframework.security.saml2.core.TestSaml2X509Credentials;
 import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.AssertionValidator;
 import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseToken;
+import org.springframework.security.saml2.provider.service.authentication.OpenSaml5AuthenticationProvider.ResponseValidator;
 import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSaml5Objects.CustomOpenSamlObject;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
@@ -754,6 +755,22 @@ public class OpenSaml5AuthenticationProviderTests {
 		verify(validator).convert(any(OpenSaml5AuthenticationProvider.ResponseToken.class));
 	}
 
+	@Test
+	public void authenticateWhenCustomSetOfResponseValidatorsThenUses() {
+		Converter<OpenSaml5AuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> validator = mock(
+				Converter.class);
+		given(validator.convert(any()))
+			.willReturn(Saml2ResponseValidatorResult.failure(new Saml2Error("error", "description")));
+		ResponseValidator responseValidator = new ResponseValidator(validator);
+		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+		provider.setResponseValidator(responseValidator);
+		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
+		Saml2AuthenticationToken token = token(response, verifying(registration()));
+		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
+			.withMessageContaining("description");
+		verify(validator).convert(any());
+	}
+
 	@Test
 	public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() {
 		Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();