Selaa lähdekoodia

Add AssertionValidator

- Ships with support for customizing the OpenSAML validators to use
- Or, you can supply your own instance of SAML20AssertionValidator

Closes gh-15578
Josh Cummings 6 kuukautta sitten
vanhempi
commit
91b0936189

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

@@ -192,6 +192,64 @@ open class SecurityConfig {
 ----
 ======
 
+If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way, using `OpenSaml5AuthenticationProvider.AssertionValidator`:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
+        AssertionValidator assertionValidator = AssertionValidator.builder()
+                .clockSkew(Duration.ofMinutes(10)).build();
+		authenticationProvider.setAssertionValidator(assertionValidator);
+        http
+            .authorizeHttpRequests(authz -> authz
+                .anyRequest().authenticated()
+            )
+            .saml2Login(saml2 -> saml2
+                .authenticationManager(new ProviderManager(authenticationProvider))
+            );
+        return http.build();
+	}
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+
+
+@Configuration @EnableWebSecurity
+class SecurityConfig {
+    @Bean
+    @Throws(Exception::class)
+    fun filterChain(http: HttpSecurity): SecurityFilterChain {
+        val authenticationProvider = OpenSaml5AuthenticationProvider()
+        val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
+        authenticationProvider.setAssertionValidator(assertionValidator)
+        http {
+            authorizeHttpRequests {
+                authorize(anyRequest, authenticated)
+            }
+            saml2Login {
+                authenticationManager = ProviderManager(authenticationProvider)
+            }
+        }
+        return http.build()
+    }
+}
+----
+======
+
 [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
 == Coordinating with a `UserDetailsService`
 
@@ -368,6 +426,60 @@ provider.setAssertionValidator { assertionToken ->
 While recommended, it's not necessary to call ``OpenSaml4AuthenticationProvider``'s default assertion validator.
 A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
 
+If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way using `OpenSaml5AuthenticationProvider.AssertionValidator`:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+OneTimeUseConditionValidator validator = ...;
+AssertionValidator assertionValidator = AssertionValidator.builder()
+        .conditionValidators((c) -> c.add(validator)).build();
+provider.setAssertionValidator(assertionValidator);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+val provider = OpenSaml5AuthenticationProvider()
+val validator: OneTimeUseConditionValidator = ...;
+val assertionValidator = AssertionValidator.builder()
+        .conditionValidators { add(validator) }.build()
+provider.setAssertionValidator(assertionValidator)
+----
+======
+
+You can use this same builder to remove validators that you don't want to use like so:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+AssertionValidator assertionValidator = AssertionValidator.builder()
+        .conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
+provider.setAssertionValidator(assertionValidator);
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+val provider = new OpenSaml5AuthenticationProvider()
+val assertionValidator = AssertionValidator.builder()
+        .conditionValidators {
+			c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
+        }.build()
+provider.setAssertionValidator(assertionValidator)
+----
+======
+
 [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
 == Customizing Decryption
 

+ 315 - 87
saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java

@@ -17,21 +17,40 @@
 package org.springframework.security.saml2.provider.service.authentication;
 
 import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+import org.opensaml.saml.common.assertion.AssertionValidationException;
 import org.opensaml.saml.common.assertion.ValidationContext;
 import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.saml2.assertion.ConditionValidator;
 import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
 import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
+import org.opensaml.saml.saml2.assertion.StatementValidator;
+import org.opensaml.saml.saml2.assertion.SubjectConfirmationValidator;
+import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator;
+import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator;
+import org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator;
+import org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator;
 import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Condition;
 import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.OneTimeUse;
 import org.opensaml.saml.saml2.core.Response;
 import org.opensaml.saml.saml2.core.SubjectConfirmation;
 import org.opensaml.saml.saml2.core.SubjectConfirmationData;
 import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
+import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
 
 import org.springframework.core.convert.converter.Converter;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
@@ -95,7 +114,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 */
 	public OpenSaml5AuthenticationProvider() {
 		this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
-		setAssertionValidator(createDefaultAssertionValidator());
+		setAssertionValidator(AssertionValidator.withDefaults());
 	}
 
 	/**
@@ -173,14 +192,14 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 * Set the {@link Converter} to use for validating each {@link Assertion} in the SAML
 	 * 2.0 Response.
 	 *
-	 * You can still invoke the default validator by delgating to
-	 * {@link #createAssertionValidator}, like so:
+	 * You can still invoke the default validator by calling
+	 * {@link AssertionValidator#withDefaults()}, like so:
 	 *
 	 * <pre>
 	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
+	 *  AssertionValidator validator = AssertionValidator.withDefaults();
 	 *  provider.setAssertionValidator(assertionToken -&gt; {
-	 *		Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
-	 *			.convert(assertionToken)
+	 *		Saml2ResponseValidatorResult result = validator.validate(assertionToken);
 	 *		return result.concat(myCustomValidator.convert(assertionToken));
 	 *  });
 	 * </pre>
@@ -190,17 +209,12 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 *
 	 * <pre>
 	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
-	 *	provider.setAssertionValidator(
-	 *		createDefaultAssertionValidator(assertionToken -&gt; {
-	 *			Map&lt;String, Object&gt; params = new HashMap&lt;&gt;();
-	 *			params.put(CLOCK_SKEW, 2 * 60 * 1000);
-	 *			// other parameters
-	 *			return new ValidationContext(params);
-	 *		}));
+	 *  AssertionValidator validator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(2)).build();
+	 *	provider.setAssertionValidator(validator);
 	 * </pre>
 	 *
-	 * Consider taking a look at {@link #createValidationContext} to see how it constructs
-	 * a {@link ValidationContext}.
+	 * Consider taking a look at {@link AssertionValidator#createValidationContext} to see
+	 * how it constructs a {@link ValidationContext}.
 	 *
 	 * It is not necessary to delegate to the default validator. You can safely replace it
 	 * entirely with your own. Note that signature verification is performed as a separate
@@ -299,10 +313,11 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 * Construct a default strategy for validating each SAML 2.0 Assertion and associated
 	 * {@link Authentication} token
 	 * @return the default assertion validator strategy
+	 * @deprecated please use {@link AssertionValidator#withDefaults()} instead
 	 */
+	@Deprecated
 	public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator() {
-		return createDefaultAssertionValidatorWithParameters(
-				(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5)));
+		return AssertionValidator.withDefaults();
 	}
 
 	/**
@@ -316,9 +331,25 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	@Deprecated
 	public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
 			Converter<AssertionToken, ValidationContext> contextConverter) {
-		return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
-				(assertionToken) -> BaseOpenSamlAuthenticationProvider.SAML20AssertionValidators.attributeValidator,
-				contextConverter);
+		return (assertionToken) -> {
+			Assertion assertion = assertionToken.getAssertion();
+			SAML20AssertionValidator validator = BaseOpenSamlAuthenticationProvider.SAML20AssertionValidators.attributeValidator;
+			ValidationContext context = contextConverter.convert(assertionToken);
+			try {
+				ValidationResult result = validator.validate(assertion, context);
+				if (result == ValidationResult.VALID) {
+					return Saml2ResponseValidatorResult.success();
+				}
+			}
+			catch (Exception ex) {
+				String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
+						((Response) assertion.getParent()).getID(), ex.getMessage());
+				return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ASSERTION, message));
+			}
+			String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
+					((Response) assertion.getParent()).getID(), context.getValidationFailureMessages());
+			return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ASSERTION, message));
+		};
 	}
 
 	/**
@@ -328,12 +359,12 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 	 * {@link ValidationContext} for each assertion being validated
 	 * @return the default assertion validator strategy
 	 * @since 5.8
+	 * @deprecated please use {@link AssertionValidator#withDefaults()} instead
 	 */
+	@Deprecated
 	public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters(
 			Consumer<Map<String, Object>> validationContextParameters) {
-		return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION,
-				(assertionToken) -> BaseOpenSamlAuthenticationProvider.SAML20AssertionValidators.attributeValidator,
-				(assertionToken) -> createValidationContext(assertionToken, validationContextParameters));
+		return AssertionValidator.builder().validationContextParameters(validationContextParameters).build();
 	}
 
 	/**
@@ -364,71 +395,6 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 		return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication);
 	}
 
-	private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAssertionValidator(String errorCode,
-			Converter<AssertionToken, SAML20AssertionValidator> validatorConverter,
-			Converter<AssertionToken, ValidationContext> contextConverter) {
-
-		return (assertionToken) -> {
-			Assertion assertion = assertionToken.getAssertion();
-			SAML20AssertionValidator validator = validatorConverter.convert(assertionToken);
-			ValidationContext context = contextConverter.convert(assertionToken);
-			try {
-				ValidationResult result = validator.validate(assertion, context);
-				if (result == ValidationResult.VALID) {
-					return Saml2ResponseValidatorResult.success();
-				}
-			}
-			catch (Exception ex) {
-				String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
-						((Response) assertion.getParent()).getID(), ex.getMessage());
-				return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
-			}
-			String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
-					((Response) assertion.getParent()).getID(), context.getValidationFailureMessages());
-			return Saml2ResponseValidatorResult.failure(new Saml2Error(errorCode, message));
-		};
-	}
-
-	private static ValidationContext createValidationContext(AssertionToken assertionToken,
-			Consumer<Map<String, Object>> paramsConsumer) {
-		Saml2AuthenticationToken token = assertionToken.getToken();
-		RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration();
-		String audience = relyingPartyRegistration.getEntityId();
-		String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation();
-		String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyMetadata().getEntityId();
-		Map<String, Object> params = new HashMap<>();
-		Assertion assertion = assertionToken.getAssertion();
-		if (assertionContainsInResponseTo(assertion)) {
-			String requestId = getAuthnRequestId(token.getAuthenticationRequest());
-			params.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, requestId);
-		}
-		params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
-		params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
-		params.put(SAML2AssertionValidationParameters.VALID_ISSUERS, Collections.singleton(assertingPartyEntityId));
-		paramsConsumer.accept(params);
-		return new ValidationContext(params);
-	}
-
-	private static boolean assertionContainsInResponseTo(Assertion assertion) {
-		if (assertion.getSubject() == null) {
-			return false;
-		}
-		for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
-			SubjectConfirmationData confirmationData = confirmation.getSubjectConfirmationData();
-			if (confirmationData == null) {
-				continue;
-			}
-			if (StringUtils.hasText(confirmationData.getInResponseTo())) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	private static String getAuthnRequestId(AbstractSaml2AuthenticationRequest serialized) {
-		return (serialized != null) ? serialized.getId() : null;
-	}
-
 	/**
 	 * A tuple containing an OpenSAML {@link Response} and its associated authentication
 	 * token.
@@ -493,4 +459,266 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
 
 	}
 
+	/**
+	 * A default implementation of {@link OpenSaml5AuthenticationProvider}'s assertion
+	 * validator. This does not check the signature as signature verification is performed
+	 * by a different component
+	 *
+	 * @author Josh Cummings
+	 * @since 6.5
+	 */
+	public static final class AssertionValidator implements Converter<AssertionToken, Saml2ResponseValidatorResult> {
+
+		private final SAML20AssertionValidator assertionValidator;
+
+		private Consumer<Map<String, Object>> paramsConsumer = (map) -> {
+		};
+
+		public AssertionValidator(SAML20AssertionValidator assertionValidator) {
+			this.assertionValidator = assertionValidator;
+		}
+
+		@Override
+		public Saml2ResponseValidatorResult convert(AssertionToken source) {
+			Assertion assertion = source.getAssertion();
+			ValidationContext validationContext = createValidationContext(source);
+			try {
+				ValidationResult result = this.assertionValidator.validate(assertion, validationContext);
+				if (result == ValidationResult.VALID) {
+					return Saml2ResponseValidatorResult.success();
+				}
+			}
+			catch (Exception ex) {
+				String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
+						((Response) assertion.getParent()).getID(), ex.getMessage());
+				return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ASSERTION, message));
+			}
+			String message = String.format("Invalid assertion [%s] for SAML response [%s]: %s", assertion.getID(),
+					((Response) assertion.getParent()).getID(), validationContext.getValidationFailureMessages());
+			return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ASSERTION, message));
+		}
+
+		/**
+		 * Validate this assertion
+		 * @param token the assertion to validate
+		 * @return the validation result
+		 */
+		public Saml2ResponseValidatorResult validate(AssertionToken token) {
+			return convert(token);
+		}
+
+		/**
+		 * Mutate the map of OpenSAML {@link ValidationContext} parameters using the given
+		 * {@code paramsConsumer}
+		 * @param paramsConsumer the context parameters mutator
+		 */
+		public void setValidationContextParameters(Consumer<Map<String, Object>> paramsConsumer) {
+			this.paramsConsumer = paramsConsumer;
+		}
+
+		private ValidationContext createValidationContext(AssertionToken assertionToken) {
+			Saml2AuthenticationToken token = assertionToken.getToken();
+			RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration();
+			String audience = relyingPartyRegistration.getEntityId();
+			String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation();
+			String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyMetadata().getEntityId();
+			Map<String, Object> params = new HashMap<>();
+			Assertion assertion = assertionToken.getAssertion();
+			if (assertionContainsInResponseTo(assertion)) {
+				String requestId = getAuthnRequestId(token.getAuthenticationRequest());
+				params.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, requestId);
+			}
+			params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
+			params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
+			params.put(SAML2AssertionValidationParameters.VALID_ISSUERS, Collections.singleton(assertingPartyEntityId));
+			params.put(SAML2AssertionValidationParameters.SC_CHECK_ADDRESS, false);
+			this.paramsConsumer.accept(params);
+			return new ValidationContext(params);
+		}
+
+		private static boolean assertionContainsInResponseTo(Assertion assertion) {
+			if (assertion.getSubject() == null) {
+				return false;
+			}
+			for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
+				SubjectConfirmationData confirmationData = confirmation.getSubjectConfirmationData();
+				if (confirmationData == null) {
+					continue;
+				}
+				if (StringUtils.hasText(confirmationData.getInResponseTo())) {
+					return true;
+				}
+			}
+			return false;
+		}
+
+		private static String getAuthnRequestId(AbstractSaml2AuthenticationRequest serialized) {
+			return (serialized != null) ? serialized.getId() : null;
+		}
+
+		/**
+		 * Create the default assertion validator
+		 * @return the default assertion validator
+		 */
+		public static AssertionValidator withDefaults() {
+			return new Builder().build();
+		}
+
+		/**
+		 * Use a builder to configure aspects of the validator
+		 * @return the {@link Builder} for configuration {@link AssertionValidator}
+		 */
+		public static Builder builder() {
+			return new Builder();
+		}
+
+		public static final class Builder {
+
+			private final List<ConditionValidator> conditions = new ArrayList<>();
+
+			private final List<SubjectConfirmationValidator> subjects = new ArrayList<>();
+
+			private final Map<String, Object> validationParameters = new HashMap<>();
+
+			private Builder() {
+				this.conditions.add(new AudienceRestrictionConditionValidator());
+				this.conditions.add(new DelegationRestrictionConditionValidator());
+				this.conditions.add(new ValidConditionValidator(OneTimeUse.DEFAULT_ELEMENT_NAME));
+				this.conditions.add(new ProxyRestrictionConditionValidator());
+				this.subjects.add(new BearerSubjectConfirmationValidator());
+				this.validationParameters.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5));
+			}
+
+			/**
+			 * Use this clock skew for validating assertion timestamps. The default is 5
+			 * minutes.
+			 * @param duration the duration to use
+			 * @return the {@link Builder} for further configuration
+			 */
+			public Builder clockSkew(Duration duration) {
+				this.validationParameters.put(SAML2AssertionValidationParameters.CLOCK_SKEW, duration);
+				return this;
+			}
+
+			/**
+			 * Mutate the map of {@link ValidationContext} static parameters. By default,
+			 * these include:
+			 * <ul>
+			 * <li>{@link SAML2AssertionValidationParameters#SC_VALID_IN_RESPONSE_TO}</li>>
+			 * <li>{@link SAML2AssertionValidationParameters#COND_VALID_AUDIENCES}</li>>
+			 * <li>{@link SAML2AssertionValidationParameters#SC_VALID_RECIPIENTS}</li>>
+			 * <li>{@link SAML2AssertionValidationParameters#VALID_ISSUERS}</li>>
+			 * <li>{@link SAML2AssertionValidationParameters#SC_CHECK_ADDRESS}</li>>
+			 * <li>{@link SAML2AssertionValidationParameters#CLOCK_SKEW}</li>>
+			 * </ul>
+			 *
+			 * Note that several of these are required by various validation steps, for
+			 * example {@code COND_VALID_AUDIENCES} is needed by
+			 * {@link BearerSubjectConfirmationValidator}. If you do not want these, the
+			 * best way to remove them is to remove the {@link #conditionValidators} or
+			 * {@link #subjectValidators} themselves
+			 * @param parameters the mutator to change the set of parameters
+			 * @return
+			 */
+			public Builder validationContextParameters(Consumer<Map<String, Object>> parameters) {
+				parameters.accept(this.validationParameters);
+				return this;
+			}
+
+			/**
+			 * Mutate the list of {@link ConditionValidator}s. By default, these include:
+			 * <ul>
+			 * <li>{@link AudienceRestrictionConditionValidator}</li>
+			 * <li>{@link DelegationRestrictionConditionValidator}</li>
+			 * <li>{@link ProxyRestrictionConditionValidator}</li>
+			 * </ul>
+			 * Note that it also adds a validator that skips the {@code saml2:OneTimeUse}
+			 * element since this validator does not have caching facilities. However, you
+			 * can construct your own instance of
+			 * {@link org.opensaml.saml.saml2.assertion.impl.OneTimeUseConditionValidator}
+			 * and supply it here.
+			 * @param conditions the mutator for changing the list of conditions to use
+			 * @return the {@link Builder} for further configuration
+			 */
+			public Builder conditionValidators(Consumer<List<ConditionValidator>> conditions) {
+				conditions.accept(this.conditions);
+				return this;
+			}
+
+			/**
+			 * Mutate the list of {@link ConditionValidator}s.
+			 * <p>
+			 * By default it only has {@link BearerSubjectConfirmationValidator} for which
+			 * address validation is skipped.
+			 *
+			 * To turn address validation on, use
+			 * {@link #validationContextParameters(Consumer)} to set the
+			 * {@link SAML2AssertionValidationParameters#SC_CHECK_ADDRESS} value.
+			 * @param subjects the mutator for changing the list of conditions to use
+			 * @return the {@link Builder} for further configuration
+			 */
+			public Builder subjectValidators(Consumer<List<SubjectConfirmationValidator>> subjects) {
+				subjects.accept(this.subjects);
+				return this;
+			}
+
+			/**
+			 * Build the {@link AssertionValidator}
+			 * @return the {@link AssertionValidator}
+			 */
+			public AssertionValidator build() {
+				AssertionValidator validator = new AssertionValidator(new ValidSignatureAssertionValidator(
+						this.conditions, this.subjects, List.of(), null, null, null));
+				validator.setValidationContextParameters((params) -> params.putAll(this.validationParameters));
+				return validator;
+			}
+
+		}
+
+		private static final class ValidConditionValidator implements ConditionValidator {
+
+			private final QName name;
+
+			private ValidConditionValidator(QName name) {
+				this.name = name;
+			}
+
+			@Nonnull
+			@Override
+			public QName getServicedCondition() {
+				return this.name;
+			}
+
+			@Nonnull
+			@Override
+			public ValidationResult validate(@Nonnull Condition condition, @Nonnull Assertion assertion,
+					@Nonnull ValidationContext context) {
+				return ValidationResult.VALID;
+			}
+
+		}
+
+		private static final class ValidSignatureAssertionValidator extends SAML20AssertionValidator {
+
+			private ValidSignatureAssertionValidator(@Nullable Collection<ConditionValidator> newConditionValidators,
+					@Nullable Collection<SubjectConfirmationValidator> newConfirmationValidators,
+					@Nullable Collection<StatementValidator> newStatementValidators,
+					@Nullable org.opensaml.saml.saml2.assertion.AssertionValidator newAssertionValidator,
+					@Nullable SignatureTrustEngine newTrustEngine,
+					@Nullable SignaturePrevalidator newSignaturePrevalidator) {
+				super(newConditionValidators, newConfirmationValidators, newStatementValidators, newAssertionValidator,
+						newTrustEngine, newSignaturePrevalidator);
+			}
+
+			@Nonnull
+			@Override
+			protected ValidationResult validateSignature(@Nonnull Assertion token, @Nonnull ValidationContext context)
+					throws AssertionValidationException {
+				return ValidationResult.VALID;
+			}
+
+		}
+
+	}
+
 }

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

@@ -39,11 +39,14 @@ import org.opensaml.core.xml.schema.XSDateTime;
 import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
 import org.opensaml.saml.common.SignableSAMLObject;
 import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.saml2.assertion.ConditionValidator;
 import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
 import org.opensaml.saml.saml2.core.Assertion;
 import org.opensaml.saml.saml2.core.Attribute;
 import org.opensaml.saml.saml2.core.AttributeStatement;
 import org.opensaml.saml.saml2.core.AttributeValue;
+import org.opensaml.saml.saml2.core.AudienceRestriction;
 import org.opensaml.saml.saml2.core.Conditions;
 import org.opensaml.saml.saml2.core.EncryptedAssertion;
 import org.opensaml.saml.saml2.core.EncryptedAttribute;
@@ -73,6 +76,7 @@ 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.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.TestCustomOpenSaml5Objects.CustomOpenSamlObject;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
@@ -556,6 +560,25 @@ public class OpenSaml5AuthenticationProviderTests {
 		verify(validator).convert(any(OpenSaml5AuthenticationProvider.AssertionToken.class));
 	}
 
+	@Test
+	public void authenticateWhenAssertionValidatorListThenUses() throws Exception {
+		ConditionValidator custom = mock(ConditionValidator.class);
+		given(custom.getServicedCondition()).willReturn(AudienceRestriction.DEFAULT_ELEMENT_NAME);
+		given(custom.validate(any(), any(), any())).willReturn(ValidationResult.INVALID);
+		AssertionValidator validator = AssertionValidator.builder().conditionValidators((c) -> c.add(custom)).build();
+		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
+		provider.setAssertionValidator(validator);
+		Response response = TestOpenSamlObjects.signedResponseWithOneAssertion((r) -> r.getAssertions()
+			.get(0)
+			.getConditions()
+			.getConditions()
+			.add(build(AudienceRestriction.DEFAULT_ELEMENT_NAME)));
+		Saml2AuthenticationToken token = token(response, verifying(registration()));
+		assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
+			.withMessageContaining("AudienceRestriction");
+		verify(custom).validate(any(), any(), any());
+	}
+
 	@Test
 	public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
 		OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();