mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-24 04:52:16 +00:00
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:
parent
2885b0f75f
commit
91b0936189
@ -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
|
||||
|
||||
|
@ -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 -> {
|
||||
* 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 -> {
|
||||
* 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);
|
||||
* </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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user