parent
47e1fc045f
commit
3e686abf50
|
@ -359,6 +359,30 @@ provider.setResponseValidator((responseToken) -> {
|
||||||
});
|
});
|
||||||
----
|
----
|
||||||
|
|
||||||
|
When using `OpenSaml5AuthenticationProvider`, you can do the same with less boilerplate:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||||
|
ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
|
||||||
|
provider.setResponseValidator(responseValidator);
|
||||||
|
----
|
||||||
|
|
||||||
|
You can also customize which validation steps Spring Security should do.
|
||||||
|
For example, if you want to skip `Response#InResponseTo` validation, you can call ``ResponseValidator``'s constructor, excluding `InResponseToValidator` from the list:
|
||||||
|
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||||
|
ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
|
||||||
|
provider.setResponseValidator(responseValidator);
|
||||||
|
----
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
====
|
||||||
|
OpenSAML performs `Asssertion#InResponseTo` validation in its `BearerSubjectConfirmationValidator` class, which is configurable using <<_performing_additional_assertion_validation, setAssertionValidator>>.
|
||||||
|
====
|
||||||
|
|
||||||
== Performing Additional Assertion Validation
|
== Performing Additional Assertion Validation
|
||||||
`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
||||||
After verifying the signature, it will:
|
After verifying the signature, it will:
|
||||||
|
|
|
@ -183,7 +183,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<String> getStatusCodes(Response response) {
|
static List<String> getStatusCodes(Response response) {
|
||||||
if (response.getStatus() == null) {
|
if (response.getStatus() == null) {
|
||||||
return List.of(StatusCode.SUCCESS);
|
return List.of(StatusCode.SUCCESS);
|
||||||
}
|
}
|
||||||
|
@ -206,7 +206,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||||
return List.of(parentStatusCodeValue, childStatusCodeValue);
|
return List.of(parentStatusCodeValue, childStatusCodeValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isSuccess(List<String> statusCodes) {
|
static boolean isSuccess(List<String> statusCodes) {
|
||||||
if (statusCodes.size() != 1) {
|
if (statusCodes.size() != 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -215,7 +215,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
||||||
return StatusCode.SUCCESS.equals(statusCode);
|
return StatusCode.SUCCESS.equals(statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
|
static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
|
||||||
String inResponseTo) {
|
String inResponseTo) {
|
||||||
if (!StringUtils.hasText(inResponseTo)) {
|
if (!StringUtils.hasText(inResponseTo)) {
|
||||||
return Saml2ResponseValidatorResult.success();
|
return Saml2ResponseValidatorResult.success();
|
||||||
|
|
|
@ -53,6 +53,7 @@ import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
|
||||||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
@ -60,6 +61,7 @@ import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.saml2.core.Saml2Error;
|
import org.springframework.security.saml2.core.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.provider.service.registration.AssertingPartyMetadata;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
@ -114,6 +116,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||||
*/
|
*/
|
||||||
public OpenSaml5AuthenticationProvider() {
|
public OpenSaml5AuthenticationProvider() {
|
||||||
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
|
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
|
||||||
|
setResponseValidator(ResponseValidator.withDefaults());
|
||||||
setAssertionValidator(AssertionValidator.withDefaults());
|
setAssertionValidator(AssertionValidator.withDefaults());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,12 +304,11 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||||
* Construct a default strategy for validating the SAML 2.0 Response
|
* Construct a default strategy for validating the SAML 2.0 Response
|
||||||
* @return the default response validator strategy
|
* @return the default response validator strategy
|
||||||
* @since 5.6
|
* @since 5.6
|
||||||
|
* @deprecated please use {@link ResponseValidator#withDefaults()} instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
||||||
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
|
return ResponseValidator.withDefaults();
|
||||||
.createDefaultResponseValidator();
|
|
||||||
return (token) -> delegate
|
|
||||||
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -459,6 +461,135 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response validator that checks the {@code InResponseTo} value against the
|
||||||
|
* correlating {@link AbstractSaml2AuthenticationRequest}
|
||||||
|
*
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public static final class InResponseToValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
|
||||||
|
AbstractSaml2AuthenticationRequest request = responseToken.getToken().getAuthenticationRequest();
|
||||||
|
Response response = responseToken.getResponse();
|
||||||
|
String inResponseTo = response.getInResponseTo();
|
||||||
|
return BaseOpenSamlAuthenticationProvider.validateInResponseTo(request, inResponseTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response validator that compares the {@code Destination} value to the configured
|
||||||
|
* {@link RelyingPartyRegistration#getAssertionConsumerServiceLocation()}
|
||||||
|
*
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public static final class DestinationValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
|
||||||
|
Response response = responseToken.getResponse();
|
||||||
|
Saml2AuthenticationToken token = responseToken.getToken();
|
||||||
|
String destination = response.getDestination();
|
||||||
|
String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
|
||||||
|
if (StringUtils.hasText(destination) && !destination.equals(location)) {
|
||||||
|
String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID()
|
||||||
|
+ "]";
|
||||||
|
return Saml2ResponseValidatorResult
|
||||||
|
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message));
|
||||||
|
}
|
||||||
|
return Saml2ResponseValidatorResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response validator that compares the {@code Issuer} value to the configured
|
||||||
|
* {@link AssertingPartyMetadata#getEntityId()}
|
||||||
|
*
|
||||||
|
* @since 6.5
|
||||||
|
*/
|
||||||
|
public static final class IssuerValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
|
||||||
|
Response response = responseToken.getResponse();
|
||||||
|
Saml2AuthenticationToken token = responseToken.getToken();
|
||||||
|
String issuer = response.getIssuer().getValue();
|
||||||
|
String assertingPartyEntityId = token.getRelyingPartyRegistration()
|
||||||
|
.getAssertingPartyMetadata()
|
||||||
|
.getEntityId();
|
||||||
|
if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) {
|
||||||
|
String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID());
|
||||||
|
return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message));
|
||||||
|
}
|
||||||
|
return Saml2ResponseValidatorResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composite response validator that confirms a {@code SUCCESS} status, that there
|
||||||
|
* is at least one assertion, and any other configured converters
|
||||||
|
*
|
||||||
|
* @since 6.5
|
||||||
|
* @see InResponseToValidator
|
||||||
|
* @see DestinationValidator
|
||||||
|
* @see IssuerValidator
|
||||||
|
*/
|
||||||
|
public static final class ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
|
||||||
|
|
||||||
|
private static final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> DEFAULTS = List
|
||||||
|
.of(new InResponseToValidator(), new DestinationValidator(), new IssuerValidator());
|
||||||
|
|
||||||
|
private final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> validators;
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public ResponseValidator(Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) {
|
||||||
|
this.validators = List.of(validators);
|
||||||
|
Assert.notEmpty(this.validators, "validators cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResponseValidator withDefaults() {
|
||||||
|
return new ResponseValidator(new InResponseToValidator(), new DestinationValidator(),
|
||||||
|
new IssuerValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public static ResponseValidator withDefaults(
|
||||||
|
Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) {
|
||||||
|
List<Converter<ResponseToken, Saml2ResponseValidatorResult>> defaults = new ArrayList<>(DEFAULTS);
|
||||||
|
defaults.addAll(List.of(validators));
|
||||||
|
return new ResponseValidator(defaults.toArray(Converter[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Saml2ResponseValidatorResult convert(ResponseToken responseToken) {
|
||||||
|
Response response = responseToken.getResponse();
|
||||||
|
Collection<Saml2Error> errors = new ArrayList<>();
|
||||||
|
List<String> statusCodes = BaseOpenSamlAuthenticationProvider.getStatusCodes(response);
|
||||||
|
if (!BaseOpenSamlAuthenticationProvider.isSuccess(statusCodes)) {
|
||||||
|
for (String statusCode : statusCodes) {
|
||||||
|
String message = String.format("Invalid status [%s] for SAML response [%s]", statusCode,
|
||||||
|
response.getID());
|
||||||
|
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Converter<ResponseToken, Saml2ResponseValidatorResult> validator : this.validators) {
|
||||||
|
errors.addAll(validator.convert(responseToken).getErrors());
|
||||||
|
}
|
||||||
|
if (response.getAssertions().isEmpty()) {
|
||||||
|
errors.add(new Saml2Error(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response."));
|
||||||
|
}
|
||||||
|
return Saml2ResponseValidatorResult.failure(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default implementation of {@link OpenSaml5AuthenticationProvider}'s assertion
|
* A default implementation of {@link OpenSaml5AuthenticationProvider}'s assertion
|
||||||
* validator. This does not check the signature as signature verification is performed
|
* validator. This does not check the signature as signature verification is performed
|
||||||
|
|
|
@ -78,6 +78,7 @@ 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.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.OpenSaml5AuthenticationProvider.ResponseValidator;
|
||||||
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;
|
||||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||||
|
@ -754,6 +755,22 @@ public class OpenSaml5AuthenticationProviderTests {
|
||||||
verify(validator).convert(any(OpenSaml5AuthenticationProvider.ResponseToken.class));
|
verify(validator).convert(any(OpenSaml5AuthenticationProvider.ResponseToken.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authenticateWhenCustomSetOfResponseValidatorsThenUses() {
|
||||||
|
Converter<OpenSaml5AuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> validator = mock(
|
||||||
|
Converter.class);
|
||||||
|
given(validator.convert(any()))
|
||||||
|
.willReturn(Saml2ResponseValidatorResult.failure(new Saml2Error("error", "description")));
|
||||||
|
ResponseValidator responseValidator = new ResponseValidator(validator);
|
||||||
|
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
|
||||||
|
provider.setResponseValidator(responseValidator);
|
||||||
|
Response response = TestOpenSamlObjects.signedResponseWithOneAssertion();
|
||||||
|
Saml2AuthenticationToken token = token(response, verifying(registration()));
|
||||||
|
assertThatExceptionOfType(Saml2AuthenticationException.class).isThrownBy(() -> provider.authenticate(token))
|
||||||
|
.withMessageContaining("description");
|
||||||
|
verify(validator).convert(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() {
|
public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() {
|
||||||
Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();
|
Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();
|
||||||
|
|
Loading…
Reference in New Issue