diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 29c67cc8dd..9fc64c2638 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/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 `` or the `` 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 -> c.removeIf { it is AudienceRestrictionValidator } + }.build() +provider.setAssertionValidator(assertionValidator) +---- +====== + [[servlet-saml2login-opensamlauthenticationprovider-decryption]] == Customizing Decryption diff --git a/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java b/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java index fecaae570e..f6ca75e1cc 100644 --- a/saml2/saml2-service-provider/src/opensaml5Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProvider.java +++ b/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: * *
 	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
+	 *  AssertionValidator validator = AssertionValidator.withDefaults();
 	 *  provider.setAssertionValidator(assertionToken -> {
-	 *		Saml2ResponseValidatorResult result = createDefaultAssertionValidator()
-	 *			.convert(assertionToken)
+	 *		Saml2ResponseValidatorResult result = validator.validate(assertionToken);
 	 *		return result.concat(myCustomValidator.convert(assertionToken));
 	 *  });
 	 * 
@@ -190,17 +209,12 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv * *
 	 *	OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
-	 *	provider.setAssertionValidator(
-	 *		createDefaultAssertionValidator(assertionToken -> {
-	 *			Map<String, Object> params = new HashMap<>();
-	 *			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);
 	 * 
* - * 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 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 createDefaultAssertionValidator( Converter 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 createDefaultAssertionValidatorWithParameters( Consumer> 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 createAssertionValidator(String errorCode, - Converter validatorConverter, - Converter 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> paramsConsumer) { - Saml2AuthenticationToken token = assertionToken.getToken(); - RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration(); - String audience = relyingPartyRegistration.getEntityId(); - String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation(); - String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyMetadata().getEntityId(); - Map 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 { + + private final SAML20AssertionValidator assertionValidator; + + private Consumer> 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> 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 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 conditions = new ArrayList<>(); + + private final List subjects = new ArrayList<>(); + + private final Map 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: + *
    + *
  • {@link SAML2AssertionValidationParameters#SC_VALID_IN_RESPONSE_TO}
  • > + *
  • {@link SAML2AssertionValidationParameters#COND_VALID_AUDIENCES}
  • > + *
  • {@link SAML2AssertionValidationParameters#SC_VALID_RECIPIENTS}
  • > + *
  • {@link SAML2AssertionValidationParameters#VALID_ISSUERS}
  • > + *
  • {@link SAML2AssertionValidationParameters#SC_CHECK_ADDRESS}
  • > + *
  • {@link SAML2AssertionValidationParameters#CLOCK_SKEW}
  • > + *
+ * + * 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> parameters) { + parameters.accept(this.validationParameters); + return this; + } + + /** + * Mutate the list of {@link ConditionValidator}s. By default, these include: + *
    + *
  • {@link AudienceRestrictionConditionValidator}
  • + *
  • {@link DelegationRestrictionConditionValidator}
  • + *
  • {@link ProxyRestrictionConditionValidator}
  • + *
+ * 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> conditions) { + conditions.accept(this.conditions); + return this; + } + + /** + * Mutate the list of {@link ConditionValidator}s. + *

+ * 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> 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 newConditionValidators, + @Nullable Collection newConfirmationValidators, + @Nullable Collection 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; + } + + } + + } + } diff --git a/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java index 22ed0e89b6..a0ef0de8cf 100644 --- a/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java +++ b/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();