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
|
||||
`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
|
||||
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) {
|
||||
return List.of(StatusCode.SUCCESS);
|
||||
}
|
||||
|
@ -206,7 +206,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
|||
return List.of(parentStatusCodeValue, childStatusCodeValue);
|
||||
}
|
||||
|
||||
private static boolean isSuccess(List<String> statusCodes) {
|
||||
static boolean isSuccess(List<String> statusCodes) {
|
||||
if (statusCodes.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ class BaseOpenSamlAuthenticationProvider implements AuthenticationProvider {
|
|||
return StatusCode.SUCCESS.equals(statusCode);
|
||||
}
|
||||
|
||||
private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
|
||||
static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
|
||||
String inResponseTo) {
|
||||
if (!StringUtils.hasText(inResponseTo)) {
|
||||
return Saml2ResponseValidatorResult.success();
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.opensaml.xmlsec.signature.support.SignaturePrevalidator;
|
|||
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
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.Saml2ErrorCodes;
|
||||
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.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
@ -114,6 +116,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
|||
*/
|
||||
public OpenSaml5AuthenticationProvider() {
|
||||
this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
|
||||
setResponseValidator(ResponseValidator.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
|
||||
* @return the default response validator strategy
|
||||
* @since 5.6
|
||||
* @deprecated please use {@link ResponseValidator#withDefaults()} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
||||
Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider
|
||||
.createDefaultResponseValidator();
|
||||
return (token) -> delegate
|
||||
.convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken()));
|
||||
return ResponseValidator.withDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
* 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.provider.service.authentication.OpenSaml5AuthenticationProvider.AssertionValidator;
|
||||
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.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
||||
|
@ -754,6 +755,22 @@ public class OpenSaml5AuthenticationProviderTests {
|
|||
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
|
||||
public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() {
|
||||
Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token();
|
||||
|
|
Loading…
Reference in New Issue