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
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.