|
53 | 53 | import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
|
54 | 54 |
|
55 | 55 | import org.springframework.core.convert.converter.Converter;
|
| 56 | +import org.springframework.lang.NonNull; |
56 | 57 | import org.springframework.security.authentication.AbstractAuthenticationToken;
|
57 | 58 | import org.springframework.security.authentication.AuthenticationProvider;
|
58 | 59 | import org.springframework.security.core.Authentication;
|
59 | 60 | import org.springframework.security.core.AuthenticationException;
|
60 | 61 | import org.springframework.security.saml2.core.Saml2Error;
|
61 | 62 | import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
62 | 63 | import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
|
| 64 | +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; |
63 | 65 | import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
64 | 66 | import org.springframework.util.Assert;
|
65 | 67 | import org.springframework.util.StringUtils;
|
@@ -114,6 +116,7 @@ public final class OpenSaml5AuthenticationProvider implements AuthenticationProv
|
114 | 116 | */
|
115 | 117 | public OpenSaml5AuthenticationProvider() {
|
116 | 118 | this.delegate = new BaseOpenSamlAuthenticationProvider(new OpenSaml5Template());
|
| 119 | + setResponseValidator(ResponseValidator.withDefaults()); |
117 | 120 | setAssertionValidator(AssertionValidator.withDefaults());
|
118 | 121 | }
|
119 | 122 |
|
@@ -301,12 +304,11 @@ public void setResponseAuthenticationConverter(
|
301 | 304 | * Construct a default strategy for validating the SAML 2.0 Response
|
302 | 305 | * @return the default response validator strategy
|
303 | 306 | * @since 5.6
|
| 307 | + * @deprecated please use {@link ResponseValidator#withDefaults()} instead |
304 | 308 | */
|
| 309 | + @Deprecated |
305 | 310 | public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefaultResponseValidator() {
|
306 |
| - Converter<BaseOpenSamlAuthenticationProvider.ResponseToken, Saml2ResponseValidatorResult> delegate = BaseOpenSamlAuthenticationProvider |
307 |
| - .createDefaultResponseValidator(); |
308 |
| - return (token) -> delegate |
309 |
| - .convert(new BaseOpenSamlAuthenticationProvider.ResponseToken(token.getResponse(), token.getToken())); |
| 311 | + return ResponseValidator.withDefaults(); |
310 | 312 | }
|
311 | 313 |
|
312 | 314 | /**
|
@@ -459,6 +461,135 @@ public Saml2AuthenticationToken getToken() {
|
459 | 461 |
|
460 | 462 | }
|
461 | 463 |
|
| 464 | + /** |
| 465 | + * A response validator that checks the {@code InResponseTo} value against the |
| 466 | + * correlating {@link AbstractSaml2AuthenticationRequest} |
| 467 | + * |
| 468 | + * @since 6.5 |
| 469 | + */ |
| 470 | + public static final class InResponseToValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { |
| 471 | + |
| 472 | + @Override |
| 473 | + @NonNull |
| 474 | + public Saml2ResponseValidatorResult convert(ResponseToken responseToken) { |
| 475 | + AbstractSaml2AuthenticationRequest request = responseToken.getToken().getAuthenticationRequest(); |
| 476 | + Response response = responseToken.getResponse(); |
| 477 | + String inResponseTo = response.getInResponseTo(); |
| 478 | + return BaseOpenSamlAuthenticationProvider.validateInResponseTo(request, inResponseTo); |
| 479 | + } |
| 480 | + |
| 481 | + } |
| 482 | + |
| 483 | + /** |
| 484 | + * A response validator that compares the {@code Destination} value to the configured |
| 485 | + * {@link RelyingPartyRegistration#getAssertionConsumerServiceLocation()} |
| 486 | + * |
| 487 | + * @since 6.5 |
| 488 | + */ |
| 489 | + public static final class DestinationValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { |
| 490 | + |
| 491 | + @Override |
| 492 | + @NonNull |
| 493 | + public Saml2ResponseValidatorResult convert(ResponseToken responseToken) { |
| 494 | + Response response = responseToken.getResponse(); |
| 495 | + Saml2AuthenticationToken token = responseToken.getToken(); |
| 496 | + String destination = response.getDestination(); |
| 497 | + String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation(); |
| 498 | + if (StringUtils.hasText(destination) && !destination.equals(location)) { |
| 499 | + String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() |
| 500 | + + "]"; |
| 501 | + return Saml2ResponseValidatorResult |
| 502 | + .failure(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message)); |
| 503 | + } |
| 504 | + return Saml2ResponseValidatorResult.success(); |
| 505 | + } |
| 506 | + |
| 507 | + } |
| 508 | + |
| 509 | + /** |
| 510 | + * A response validator that compares the {@code Issuer} value to the configured |
| 511 | + * {@link AssertingPartyMetadata#getEntityId()} |
| 512 | + * |
| 513 | + * @since 6.5 |
| 514 | + */ |
| 515 | + public static final class IssuerValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { |
| 516 | + |
| 517 | + @Override |
| 518 | + @NonNull |
| 519 | + public Saml2ResponseValidatorResult convert(ResponseToken responseToken) { |
| 520 | + Response response = responseToken.getResponse(); |
| 521 | + Saml2AuthenticationToken token = responseToken.getToken(); |
| 522 | + String issuer = response.getIssuer().getValue(); |
| 523 | + String assertingPartyEntityId = token.getRelyingPartyRegistration() |
| 524 | + .getAssertingPartyMetadata() |
| 525 | + .getEntityId(); |
| 526 | + if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) { |
| 527 | + String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); |
| 528 | + return Saml2ResponseValidatorResult.failure(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, message)); |
| 529 | + } |
| 530 | + return Saml2ResponseValidatorResult.success(); |
| 531 | + } |
| 532 | + |
| 533 | + } |
| 534 | + |
| 535 | + /** |
| 536 | + * A composite response validator that confirms a {@code SUCCESS} status, that there |
| 537 | + * is at least one assertion, and any other configured converters |
| 538 | + * |
| 539 | + * @since 6.5 |
| 540 | + * @see InResponseToValidator |
| 541 | + * @see DestinationValidator |
| 542 | + * @see IssuerValidator |
| 543 | + */ |
| 544 | + public static final class ResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> { |
| 545 | + |
| 546 | + private static final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> DEFAULTS = List |
| 547 | + .of(new InResponseToValidator(), new DestinationValidator(), new IssuerValidator()); |
| 548 | + |
| 549 | + private final List<Converter<ResponseToken, Saml2ResponseValidatorResult>> validators; |
| 550 | + |
| 551 | + @SafeVarargs |
| 552 | + public ResponseValidator(Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) { |
| 553 | + this.validators = List.of(validators); |
| 554 | + Assert.notEmpty(this.validators, "validators cannot be empty"); |
| 555 | + } |
| 556 | + |
| 557 | + public static ResponseValidator withDefaults() { |
| 558 | + return new ResponseValidator(new InResponseToValidator(), new DestinationValidator(), |
| 559 | + new IssuerValidator()); |
| 560 | + } |
| 561 | + |
| 562 | + @SafeVarargs |
| 563 | + public static ResponseValidator withDefaults( |
| 564 | + Converter<ResponseToken, Saml2ResponseValidatorResult>... validators) { |
| 565 | + List<Converter<ResponseToken, Saml2ResponseValidatorResult>> defaults = new ArrayList<>(DEFAULTS); |
| 566 | + defaults.addAll(List.of(validators)); |
| 567 | + return new ResponseValidator(defaults.toArray(Converter[]::new)); |
| 568 | + } |
| 569 | + |
| 570 | + @Override |
| 571 | + public Saml2ResponseValidatorResult convert(ResponseToken responseToken) { |
| 572 | + Response response = responseToken.getResponse(); |
| 573 | + Collection<Saml2Error> errors = new ArrayList<>(); |
| 574 | + List<String> statusCodes = BaseOpenSamlAuthenticationProvider.getStatusCodes(response); |
| 575 | + if (!BaseOpenSamlAuthenticationProvider.isSuccess(statusCodes)) { |
| 576 | + for (String statusCode : statusCodes) { |
| 577 | + String message = String.format("Invalid status [%s] for SAML response [%s]", statusCode, |
| 578 | + response.getID()); |
| 579 | + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message)); |
| 580 | + } |
| 581 | + } |
| 582 | + for (Converter<ResponseToken, Saml2ResponseValidatorResult> validator : this.validators) { |
| 583 | + errors.addAll(validator.convert(responseToken).getErrors()); |
| 584 | + } |
| 585 | + if (response.getAssertions().isEmpty()) { |
| 586 | + errors.add(new Saml2Error(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response.")); |
| 587 | + } |
| 588 | + return Saml2ResponseValidatorResult.failure(errors); |
| 589 | + } |
| 590 | + |
| 591 | + } |
| 592 | + |
462 | 593 | /**
|
463 | 594 | * A default implementation of {@link OpenSaml5AuthenticationProvider}'s assertion
|
464 | 595 | * validator. This does not check the signature as signature verification is performed
|
|
0 commit comments