springspring-securitysamlsaml-2.0spring-saml

Configuring SAML2: Bypassing 'InResponseTo' Validation While Retaining Default Settings in OpenSaml4AuthenticationProvider


I have a Java back end application running behind Nginx and spring security for SSO .My back end app lacks a mechanism to remember cookie sessions because of which SSO request could be initiated by one java instance and assertion might reach to other instance . The existing default implementation requires the validation of the InResponseTo attribute if it's present. The default implementation is based on http session which keep tracks of session via cookies.The purpose of keeping cookie is to remember id attribute and later match with InResponseTo to protect from replay attack. InResponseTo is optional atribute in SAML2.0 spec. I want to know if there's a way to disable the InResponseTo validation while still utilizing the default validation provided by OpenSaml4AuthenticationProvider. Notably, SAML2 considers the 'InResponseTo' attribute optional. What is the best method to maintain default validation but bypass InResponseTo through back end configuration settings?

My application basic configuration looks like below .

@Configuration
public class ApplicationConfiguration {

  @Autowired
  SamlSuccessHandler successHandler;

  @Bean
  SecurityFilterChain configure(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(authorize -> authorize
        .anyRequest().authenticated()
    ).saml2Login(saml2->{
      saml2.loginProcessingUrl("/saml/SSO")
          .successHandler(successHandler);
    }).build();
  }
} 

application.yml

spring:
  security:
    saml2:
      relyingparty:
        registration:
          sap-account400:
            entity-id: my_entity_id
            identityprovider:
              entity-id: https://my-service.com
              singlesignon.sign-request: true
            assertingparty:
              metadata-uri: https://okta.com/1234
            decryption:
              credentials:
                - private-key-location: classpath:private_key_encryption.pem
                  certificate-location: classpath:certificate_encryption.crt
            signing:
              credentials:
                - private-key-location: classpath:private_key.pem
                  certificate-location: classpath:certificate.crt
            acs:
              location: http://localhost:8080/saml/SSO

Solution

  • The solution provided by @ryan-heathcote is the right approach in principle as a work-around. However, due to private fields and methods in OpenSaml4AuthenticationProvider it didn't quite work for me.

    Here is my solution:

    Create a custom authenticationProvider with a custom responseValidator and configure SAML2 auth with it, like this:

    http
    .saml2Login(...)
    .saml2Logout(...)
    .authenticationProvider(samlAuthenticationProvider(openSamlAuthenticationProvider()))
    
    
    @Bean
    protected AuthenticationProvider samlAuthenticationProvider(OpenSaml4AuthenticationProvider openSamlAuthenticationProvider, [other dependencies]) {
        return new SAMLCustomAuthenticationProvider(openSamlAuthenticationProvider, saml2CustomResponseValidator());
    }
    
    @Bean
    protected OpenSaml4AuthenticationProvider openSamlAuthenticationProvider() {
        return new OpenSaml4AuthenticationProvider();
    }
    
    @Bean
    protected Saml2CustomResponseValidator saml2CustomResponseValidator() {
        return new Saml2CustomResponseValidator();
    }
    

    (make these Beans conditional on SAML being activated, if possible)

    In the custom responseValidator, remove the first error message from the ResponseValidatorResult and extract the inResponseTo ID into a String supplier:

    @Component
    public final class Saml2CustomResponseValidator implements Converter<ResponseToken, Saml2ResponseValidatorResult> {
        private final Converter<ResponseToken, Saml2ResponseValidatorResult> delegate = OpenSaml4AuthenticationProvider.createDefaultResponseValidator();
    
        @Getter
        private Supplier<String> validInResponseToSupplier;
    
        @Override
        public Saml2ResponseValidatorResult convert(@Nonnull ResponseToken responseToken) {
            Saml2ResponseValidatorResult result = this.delegate.convert(responseToken);
    
            String inResponseTo = responseToken.getResponse().getInResponseTo();
            this.validInResponseToSupplier = () -> inResponseTo;
    
            Collection<Saml2Error> errors = removeInResponseToErrorIfPresent(result);
    
            return Saml2ResponseValidatorResult.failure(errors);
        }
    
        private Collection<Saml2Error> removeInResponseToErrorIfPresent(Saml2ResponseValidatorResult result) {
            Collection<Saml2Error> errors = Collections.emptyList();
            if (result != null) {
                errors = result.getErrors();
    
                if(errors != null) {
                    if (!errors.isEmpty()) {
                        errors = errors.stream()
                                .filter(error -> !error.getErrorCode().equals(INVALID_IN_RESPONSE_TO))
                                .toList();
                    }
                } else {
                    errors = Collections.emptyList();
                }
            }
            return errors;
        }
    }
    

    This is along the lines of the other answers (thanks for the inspiration!).

    Finally, make sure to use a customized assertionValidator with a custom context parameter consumer in the custom authenticationProvider:

    // add this to the constructor:
    this.openSamlAuthenticationProvider.setResponseValidator(this.customResponseValidator);
        this.openSamlAuthenticationProvider.setAssertionValidator(createIgnoringAssertionValidator(this.customResponseValidator));
    
    [...]
    
    private static Converter<AssertionToken, Saml2ResponseValidatorResult> createIgnoringAssertionValidator(
                Converter<ResponseToken, Saml2ResponseValidatorResult> customResponseValidator) {
    
            return OpenSaml4AuthenticationProvider.createDefaultAssertionValidatorWithParameters(validationContextParameterConsumer(((Saml2CustomResponseValidator) customResponseValidator)));
        }
    
        private static Consumer<Map<String, Object>> validationContextParameterConsumer(Saml2CustomResponseValidator saml2CustomResponseValidator) {
            return params -> {
                String dynamicValidInResponseToValue = saml2CustomResponseValidator.getValidInResponseToSupplier().get();
                params.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO, dynamicValidInResponseToValue);
            };
        }
    

    The crucial bit here is the custom parameter consumer which will overwrite the inResponseTo ID with the value retrieved dynamically and lazily on runtime from the String supplier in the custom responseValidator.