Skip to content

Commit cf29bf9

Browse files
committed
Polish InResponseTo support
- Moved methods so methods are listed before the methods they call - Adjusted exception handling so no exceptions are eaten - Adjusted so that malformed_request_data is returned with request data is malformed - Refactored methods to have only immutable method parameters - Removed usage of Stream API - Moved AuthnRequestUnmarshaller into static block so that only looked up once Issue gh-9174
1 parent 3c87854 commit cf29bf9

File tree

2 files changed

+83
-81
lines changed

2 files changed

+83
-81
lines changed

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

+82-80
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
import org.opensaml.saml.saml2.core.OneTimeUse;
6666
import org.opensaml.saml.saml2.core.Response;
6767
import org.opensaml.saml.saml2.core.StatusCode;
68-
import org.opensaml.saml.saml2.core.Subject;
6968
import org.opensaml.saml.saml2.core.SubjectConfirmation;
7069
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
7170
import org.opensaml.saml.saml2.core.impl.AuthnRequestUnmarshaller;
@@ -146,6 +145,13 @@ public final class OpenSaml4AuthenticationProvider implements AuthenticationProv
146145

147146
private final ResponseUnmarshaller responseUnmarshaller;
148147

148+
private static final AuthnRequestUnmarshaller authnRequestUnmarshaller;
149+
static {
150+
XMLObjectProviderRegistry registry = ConfigurationService.get(XMLObjectProviderRegistry.class);
151+
authnRequestUnmarshaller = (AuthnRequestUnmarshaller) registry.getUnmarshallerFactory()
152+
.getUnmarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME);
153+
}
154+
149155
private final ParserPool parserPool;
150156

151157
private final Converter<ResponseToken, Saml2ResponseValidatorResult> responseSignatureValidator = createDefaultResponseSignatureValidator();
@@ -355,37 +361,6 @@ public void setResponseAuthenticationConverter(
355361
this.responseAuthenticationConverter = responseAuthenticationConverter;
356362
}
357363

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-
389364
/**
390365
* Construct a default strategy for validating the SAML 2.0 Response
391366
* @return the default response validator strategy
@@ -428,6 +403,27 @@ public static Converter<ResponseToken, Saml2ResponseValidatorResult> createDefau
428403
};
429404
}
430405

406+
private static Saml2ResponseValidatorResult validateInResponseTo(AbstractSaml2AuthenticationRequest storedRequest,
407+
String inResponseTo) {
408+
if (!StringUtils.hasText(inResponseTo)) {
409+
return Saml2ResponseValidatorResult.success();
410+
}
411+
AuthnRequest request = parseRequest(storedRequest);
412+
if (request == null) {
413+
String message = "The response contained an InResponseTo attribute [" + inResponseTo + "]"
414+
+ " but no saved authentication request was found";
415+
return Saml2ResponseValidatorResult
416+
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
417+
}
418+
if (!inResponseTo.equals(request.getID())) {
419+
String message = "The InResponseTo attribute [" + inResponseTo + "] does not match the ID of the "
420+
+ "authentication request [" + request.getID() + "]";
421+
return Saml2ResponseValidatorResult
422+
.failure(new Saml2Error(Saml2ErrorCodes.INVALID_IN_RESPONSE_TO, message));
423+
}
424+
return Saml2ResponseValidatorResult.success();
425+
}
426+
431427
/**
432428
* Construct a default strategy for validating each SAML 2.0 Assertion and associated
433429
* {@link Authentication} token
@@ -522,28 +518,6 @@ private Response parseResponse(String response) throws Saml2Exception, Saml2Auth
522518
}
523519
}
524520

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-
547521
private void process(Saml2AuthenticationToken token, Response response) {
548522
String issuer = response.getIssuer().getValue();
549523
this.logger.debug(LogMessage.format("Processing SAML response from %s", issuer));
@@ -748,40 +722,18 @@ private static Converter<AssertionToken, Saml2ResponseValidatorResult> createAss
748722
};
749723
}
750724

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-
775725
private static ValidationContext createValidationContext(AssertionToken assertionToken,
776726
Consumer<Map<String, Object>> paramsConsumer) {
777-
RelyingPartyRegistration relyingPartyRegistration = assertionToken.token.getRelyingPartyRegistration();
727+
Saml2AuthenticationToken token = assertionToken.token;
728+
RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration();
778729
String audience = relyingPartyRegistration.getEntityId();
779730
String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation();
780731
String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyDetails().getEntityId();
781732
Map<String, Object> params = new HashMap<>();
782733
Assertion assertion = assertionToken.getAssertion();
783734
if (assertionContainsInResponseTo(assertion)) {
784-
addRequestIdToValidationContext(assertionToken.token.getAuthenticationRequest(), params);
735+
String requestId = getAuthnRequestId(token.getAuthenticationRequest());
736+
params.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, requestId);
785737
}
786738
params.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, Collections.singleton(audience));
787739
params.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, Collections.singleton(recipient));
@@ -790,6 +742,56 @@ private static ValidationContext createValidationContext(AssertionToken assertio
790742
return new ValidationContext(params);
791743
}
792744

745+
private static boolean assertionContainsInResponseTo(Assertion assertion) {
746+
if (assertion.getSubject() == null) {
747+
return false;
748+
}
749+
for (SubjectConfirmation confirmation : assertion.getSubject().getSubjectConfirmations()) {
750+
SubjectConfirmationData confirmationData = confirmation.getSubjectConfirmationData();
751+
if (confirmationData == null) {
752+
continue;
753+
}
754+
if (StringUtils.hasText(confirmationData.getInResponseTo())) {
755+
return true;
756+
}
757+
}
758+
return false;
759+
}
760+
761+
private static String getAuthnRequestId(AbstractSaml2AuthenticationRequest serialized) {
762+
AuthnRequest request = parseRequest(serialized);
763+
if (request == null) {
764+
return null;
765+
}
766+
return request.getID();
767+
}
768+
769+
private static AuthnRequest parseRequest(AbstractSaml2AuthenticationRequest request) {
770+
if (request == null) {
771+
return null;
772+
}
773+
String samlRequest = request.getSamlRequest();
774+
if (!StringUtils.hasText(samlRequest)) {
775+
return null;
776+
}
777+
if (request.getBinding() == Saml2MessageBinding.REDIRECT) {
778+
samlRequest = Saml2Utils.samlInflate(Saml2Utils.samlDecode(samlRequest));
779+
}
780+
else {
781+
samlRequest = new String(Saml2Utils.samlDecode(samlRequest), StandardCharsets.UTF_8);
782+
}
783+
try {
784+
Document document = XMLObjectProviderRegistrySupport.getParserPool()
785+
.parse(new ByteArrayInputStream(samlRequest.getBytes(StandardCharsets.UTF_8)));
786+
Element element = document.getDocumentElement();
787+
return (AuthnRequest) authnRequestUnmarshaller.unmarshall(element);
788+
}
789+
catch (Exception ex) {
790+
String message = "Failed to deserialize associated authentication request [" + ex.getMessage() + "]";
791+
throw createAuthenticationException(Saml2ErrorCodes.MALFORMED_REQUEST_DATA, message, ex);
792+
}
793+
}
794+
793795
private static class SAML20AssertionValidators {
794796

795797
private static final Collection<ConditionValidator> conditions = new ArrayList<>();

saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public void evaluateInResponseToFailsWhenInResponseToInAssertionOnlyAndCorrupted
252252
Saml2MessageBinding.POST, true);
253253
Saml2AuthenticationToken token = token(response, verifying(registration()), mockAuthenticationRequest);
254254
assertThatExceptionOfType(Saml2AuthenticationException.class)
255-
.isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("invalid_assertion");
255+
.isThrownBy(() -> this.provider.authenticate(token)).withStackTraceContaining("malformed_request_data");
256256
}
257257

258258
@Test

0 commit comments

Comments
 (0)