Add AssertionValidator

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

Closes gh-15578
This commit is contained in:
Josh Cummings 2025-02-24 22:43:27 -07:00
parent 2885b0f75f
commit 91b0936189
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
3 changed files with 450 additions and 87 deletions

View File

@ -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]] [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
== Coordinating with a `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. 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. 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]] [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
== Customizing Decryption == Customizing Decryption

View File

@ -17,21 +17,40 @@
package org.springframework.security.saml2.provider.service.authentication; package org.springframework.security.saml2.provider.service.authentication;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; 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.ValidationContext;
import org.opensaml.saml.common.assertion.ValidationResult; 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.SAML20AssertionValidator;
import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; 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.Assertion;
import org.opensaml.saml.saml2.core.Condition;
import org.opensaml.saml.saml2.core.EncryptedAssertion; 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.Response;
import org.opensaml.saml.saml2.core.SubjectConfirmation; import org.opensaml.saml.saml2.core.SubjectConfirmation;
import org.opensaml.saml.saml2.core.SubjectConfirmationData; import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.encryption.Decrypter; 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.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
@ -95,7 +114,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
*/ */
public OpenSaml5AuthenticationProvider() { public OpenSaml5AuthenticationProvider() {
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template()); 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 * Set the {@link Converter} to use for validating each {@link Assertion} in the SAML
* 2.0 Response. * 2.0 Response.
* *
* You can still invoke the default validator by delgating to * You can still invoke the default validator by calling
* {@link #createAssertionValidator}, like so: * {@link AssertionValidator#withDefaults()}, like so:
* *
* <pre> * <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider(); * OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* AssertionValidator validator = AssertionValidator.withDefaults();
* provider.setAssertionValidator(assertionToken -&gt; { * provider.setAssertionValidator(assertionToken -&gt; {
* Saml2ResponseValidatorResult result = createDefaultAssertionValidator() * Saml2ResponseValidatorResult result = validator.validate(assertionToken);
* .convert(assertionToken)
* return result.concat(myCustomValidator.convert(assertionToken)); * return result.concat(myCustomValidator.convert(assertionToken));
* }); * });
* </pre> * </pre>
@ -190,17 +209,12 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
* *
* <pre> * <pre>
* OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider(); * OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
* provider.setAssertionValidator( * AssertionValidator validator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(2)).build();
* createDefaultAssertionValidator(assertionToken -&gt; { * provider.setAssertionValidator(validator);
* Map&lt;String, Object&gt; params = new HashMap&lt;&gt;();
* params.put(CLOCK_SKEW, 2 * 60 * 1000);
* // other parameters
* return new ValidationContext(params);
* }));
* </pre> * </pre>
* *
* Consider taking a look at {@link #createValidationContext} to see how it constructs * Consider taking a look at {@link AssertionValidator#createValidationContext} to see
* a {@link ValidationContext}. * how it constructs a {@link ValidationContext}.
* *
* It is not necessary to delegate to the default validator. You can safely replace it * 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 * 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 * Construct a default strategy for validating each SAML 2.0 Assertion and associated
* {@link Authentication} token * {@link Authentication} token
* @return the default assertion validator strategy * @return the default assertion validator strategy
* @deprecated please use {@link AssertionValidator#withDefaults()} instead
*/ */
@Deprecated
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator() { public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator() {
return createDefaultAssertionValidatorWithParameters( return AssertionValidator.withDefaults();
(params) -> params.put(SAML2AssertionValidationParameters.CLOCK_SKEW, Duration.ofMinutes(5)));
} }
/** /**
@ -316,9 +331,25 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
@Deprecated @Deprecated
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator( public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidator(
Converter<AssertionToken, ValidationContext> contextConverter) { Converter<AssertionToken, ValidationContext> contextConverter) {
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION, return (assertionToken) -> {
(assertionToken) -> BaseOpenSamlAuthenticationProvider.SAML20AssertionValidators.attributeValidator, Assertion assertion = assertionToken.getAssertion();
contextConverter); 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 * {@link ValidationContext} for each assertion being validated
* @return the default assertion validator strategy * @return the default assertion validator strategy
* @since 5.8 * @since 5.8
* @deprecated please use {@link AssertionValidator#withDefaults()} instead
*/ */
@Deprecated
public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters( public static Converter<AssertionToken, Saml2ResponseValidatorResult> createDefaultAssertionValidatorWithParameters(
Consumer<Map<String, Object>> validationContextParameters) { Consumer<Map<String, Object>> validationContextParameters) {
return createAssertionValidator(Saml2ErrorCodes.INVALID_ASSERTION, return AssertionValidator.builder().validationContextParameters(validationContextParameters).build();
(assertionToken) -> BaseOpenSamlAuthenticationProvider.SAML20AssertionValidators.attributeValidator,
(assertionToken) -> createValidationContext(assertionToken, validationContextParameters));
} }
/** /**
@ -364,71 +395,6 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication); 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 * A tuple containing an OpenSAML {@link Response} and its associated authentication
* token. * 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;
}
}
}
} }

View File

@ -39,11 +39,14 @@ import org.opensaml.core.xml.schema.XSDateTime;
import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder;
import org.opensaml.saml.common.SignableSAMLObject; import org.opensaml.saml.common.SignableSAMLObject;
import org.opensaml.saml.common.assertion.ValidationContext; 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.assertion.SAML2AssertionValidationParameters;
import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.AttributeValue; 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.Conditions;
import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAssertion;
import org.opensaml.saml.saml2.core.EncryptedAttribute; 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.Saml2ErrorCodes;
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
import org.springframework.security.saml2.core.TestSaml2X509Credentials; 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.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.TestCustomOpenSaml5Objects.CustomOpenSamlObject; 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.RelyingPartyRegistration;
@ -556,6 +560,25 @@ public class OpenSaml5AuthenticationProviderTests {
verify(validator).convert(any(OpenSaml5AuthenticationProvider.AssertionToken.class)); 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 @Test
public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() { public void authenticateWhenDefaultConditionValidatorNotUsedThenSignatureStillChecked() {
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider(); OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();