Skip to content

Commit 4aa9420

Browse files
fast-reflexesjzheaux
authored andcommitted
Add support for validation of InResponseTo
Whenever an InResponseTo is present in the SAML2 response and / or any of its assertions, it will be validated against the stored SAML2 request. If the request is missing or the ID of the request does not match the InResponseTo, validation fails. If there is no InResponseTo, no validation of it is done (as opposed to checking whether there is a saved request or not and then failing based on that). Closes gh-9174
1 parent a17cf9e commit 4aa9420

File tree

3 files changed

+256
-11
lines changed

3 files changed

+256
-11
lines changed

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/core/Saml2ErrorCodes.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,13 @@ public interface Saml2ErrorCodes {
3131
*/
3232
String UNKNOWN_RESPONSE_CLASS = "unknown_response_class";
3333

34+
/**
35+
* The serialized AuthNRequest could not be deserialized correctly.
36+
*
37+
* @since 5.7
38+
*/
39+
String MALFORMED_REQUEST_DATA = "malformed_request_data";
40+
3441
/**
3542
* The response data is malformed or incomplete. An invalid XML object was received,
3643
* and XML unmarshalling failed.
@@ -116,4 +123,11 @@ public interface Saml2ErrorCodes {
116123
*/
117124
String RELYING_PARTY_REGISTRATION_NOT_FOUND = "relying_party_registration_not_found";
118125

126+
/**
127+
* The InResponseTo content of the response does not match the ID of the AuthNRequest.
128+
*
129+
* @since 5.7
130+
*/
131+
String INVALID_IN_RESPONSE_TO = "invalid_in_response_to";
132+
119133
}

saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.opensaml.core.config.ConfigurationService;
3838
import org.opensaml.core.xml.XMLObject;
3939
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
40+
import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
4041
import org.opensaml.core.xml.schema.XSAny;
4142
import org.opensaml.core.xml.schema.XSBoolean;
4243
import org.opensaml.core.xml.schema.XSBooleanValue;
@@ -57,13 +58,17 @@
5758
import org.opensaml.saml.saml2.core.Assertion;
5859
import org.opensaml.saml.saml2.core.Attribute;
5960
import org.opensaml.saml.saml2.core.AttributeStatement;
61+
import org.opensaml.saml.saml2.core.AuthnRequest;
6062
import org.opensaml.saml.saml2.core.AuthnStatement;
6163
import org.opensaml.saml.saml2.core.Condition;
6264
import org.opensaml.saml.saml2.core.EncryptedAssertion;
6365
import org.opensaml.saml.saml2.core.OneTimeUse;
6466
import org.opensaml.saml.saml2.core.Response;
6567
import org.opensaml.saml.saml2.core.StatusCode;
68+
import org.opensaml.saml.saml2.core.Subject;
6669
import org.opensaml.saml.saml2.core.SubjectConfirmation;
70+
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
71+
import org.opensaml.saml.saml2.core.impl.AuthnRequestUnmarshaller;
6772
import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller;
6873
import org.opensaml.saml.saml2.encryption.Decrypter;
6974
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
@@ -85,6 +90,7 @@
8590
import org.springframework.security.saml2.core.Saml2ErrorCodes;
8691
import org.springframework.security.saml2.core.Saml2ResponseValidatorResult;
8792
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
93+
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
8894
import org.springframework.util.Assert;
8995
import org.springframework.util.CollectionUtils;
9096
import org.springframework.util.StringUtils;
@@ -349,6 +355,37 @@ public void setResponseAuthenticationConverter(
349355
this.responseAuthenticationConverter = responseAuthenticationConverter;
350356
}
351357

358+
private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
359+
String inResponseTo) {
360+
if (!StringUtils.hasText(inResponseTo)) {
361+
return Saml2ResponseValidatorResult.success();
362+
}
363+
AuthnRequest request;
364+
try {
365+
request = parseRequest(storedRequest);
366+
}
367+
catch (Exception ex) {
368+
String message = "The stored AuthNRequest could not be properly deserialized [" + ex.getMessage() + "]";
369+
return Saml2ResponseValidatorResult
370+
.failure(new Saml2Error(Saml2ErrorCodes.MALFORMED_REQUEST_DATA, message));
371+
}
372+
if (request == null) {
373+
String message = "The response contained an InResponseTo attribute [" + inResponseTo + "]"
374+
+ " but no saved AuthNRequest request was found";
375+
return Saml2ResponseValidatorResult
376+
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
377+
}
378+
else if (!request.getID().equals(inResponseTo)) {
379+
String message = "The InResponseTo attribute [" + inResponseTo + "] does not match the ID of the "
380+
+ "AuthNRequest [" + request.getID() + "]";
381+
return Saml2ResponseValidatorResult
382+
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
383+
}
384+
else {
385+
return Saml2ResponseValidatorResult.success();
386+
}
387+
}
388+
352389
/**
353390
* Construct a default strategy for validating the SAML 2.0 Response
354391
* @return the default response validator strategy
@@ -365,6 +402,10 @@ public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefau
365402
response.getID());
366403
result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_RESPONSE, message));
367404
}
405+
406+
String inResponseTo = response.getInResponseTo();
407+
result = result.concat(validateInResponseTo(token.getAuthenticationRequest(), inResponseTo));
408+
368409
String issuer = response.getIssuer().getValue();
369410
String destination = response.getDestination();
370411
String location = token.getRelyingPartyRegistration().getAssertionConsumerServiceLocation();
@@ -447,7 +488,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
447488
try {
448489
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
449490
String serializedResponse = token.getSaml2Response();
450-
Response response = parse(serializedResponse);
491+
Response response = parseResponse(serializedResponse);
451492
process(token, response);
452493
AbstractAuthenticationToken authenticationResponse = this.responseAuthenticationConverter
453494
.convert(new ResponseToken(response, token));
@@ -469,7 +510,7 @@ public boolean supports(Class<?> authentication) {
469510
return authentication != null && Saml2AuthenticationToken.class.isAssignableFrom(authentication);
470511
}
471512

472-
private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException {
513+
private Response parseResponse(String response) throws Saml2Exception, Saml2AuthenticationException {
473514
try {
474515
Document document = this.parserPool
475516
.parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8)));
@@ -481,6 +522,28 @@ private Response parse(String response) throws Saml2Exception, Saml2Authenticati
481522
}
482523
}
483524

525+
private static AuthnRequest parseRequest(AbstractSaml2AuthenticationRequest request) throws Exception {
526+
if (request == null) {
527+
return null;
528+
}
529+
String samlRequest = request.getSamlRequest();
530+
if (!StringUtils.hasText(samlRequest)) {
531+
return null;
532+
}
533+
if (request.getBinding() == Saml2MessageBinding.REDIRECT) {
534+
samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
535+
}
536+
else {
537+
samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
538+
}
539+
Document document = XMLObjectProviderRegistrySupport.getParserPool()
540+
.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
541+
Element element = document.getDocumentElement();
542+
AuthnRequestUnmarshaller unmarshaller = (AuthnRequestUnmarshaller) XMLObjectProviderRegistrySupport
543+
.getUnmarshallerFactory().getUnmarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME);
544+
return (AuthnRequest) unmarshaller.unmarshall(element);
545+
}
546+
484547
private void process(Saml2AuthenticationToken token, Response response) {
485548
String issuer = response.getIssuer().getValue();
486549
this.logger.debug(LogMessage.format("Processing SAML response from %s", issuer));
@@ -685,13 +748,41 @@ private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAss
685748
};
686749
}
687750

751+
private static boolean assertionContainsInResponseTo(Assertion assertion) {
752+
Subject subject = (assertion != null) ? assertion.getSubject() : null;
753+
List<SubjectConfirmation> confirmations = (subject != null) ? subject.getSubjectConfirmations()
754+
: new ArrayList<>();
755+
return confirmations.stream().filter((confirmation) -> {
756+
SubjectConfirmationData confirmationData = confirmation.getSubjectConfirmationData();
757+
return confirmationData != null && StringUtils.hasText(confirmationData.getInResponseTo());
758+
}).findFirst().orElse(null) != null;
759+
}
760+
761+
private static void addRequestIdToValidationContext(AbstractSaml2AuthenticationRequest storedRequest,
762+
Map<String, Object> context) {
763+
String requestId = null;
764+
try {
765+
AuthnRequest request = parseRequest(storedRequest);
766+
requestId = (request != null) ? request.getID() : null;
767+
}
768+
catch (Exception ex) {
769+
}
770+
if (StringUtils.hasText(requestId)) {
771+
context.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, requestId);
772+
}
773+
}
774+
688775
private static ValidationContext createValidationContext(AssertionToken assertionToken,
689776
Consumer<Map<String, Object>> paramsConsumer) {
690777
RelyingPartyRegistration relyingPartyRegistration = assertionToken.token.getRelyingPartyRegistration();
691778
String audience = relyingPartyRegistration.getEntityId();
692779
String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation();
693780
String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyDetails().getEntityId();
694781
Map<String, Object> params = new HashMap<>();
782+
Assertion assertion = assertionToken.getAssertion();
783+
if (assertionContainsInResponseTo(assertion)) {
784+
addRequestIdToValidationContext(assertionToken.token.getAuthenticationRequest(), params);
785+
}
695786
params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
696787
params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
697788
params.put(SAML2AssertionValidationParameters.VALID_ISSUERS, Collections.singleton(assertingPartyEntityId));
@@ -733,13 +824,6 @@ protected ValidationResult validateAddress(SubjectConfirmation confirmation, Ass
733824
// applications should validate their own addresses - gh-7514
734825
return ValidationResult.VALID;
735826
}
736-
737-
@Override
738-
protected ValidationResult validateInResponseTo(SubjectConfirmation confirmation, Assertion assertion,
739-
ValidationContext context, boolean required) {
740-
// applications should validate their own in response to
741-
return ValidationResult.VALID;
742-
}
743827
});
744828
}
745829

0 commit comments

Comments
 (0)