Describe the bug When a SAML IdP is configured to add a OneTimeUse condition to the SAML Assertion, the OpenSamlAuthenticationProvider throws a Saml2Exception and claims that the OneTimeUse condition is an unknown condition.
To Reproduce
- Configure an IdP to send the OneTimeUse condition
- Attempt to authenticate to the application
- Authentication fails due to the Saml2Exception
In Keycloak, the client registration can be configured with the Include OneTimeUse Condition slider to replicate this issue.
Expected behavior This condition should be parsed and handled appropriately.
Debug Log output
2020-06-26 10:30:35.002 DEBUG 12544 --- [nio-8080-exec-3] .p.s.s.f.Saml2WebSsoAuthenticationFilter : Request is to process authentication
2020-06-26 10:30:35.070 DEBUG 12544 --- [nio-8080-exec-3] s.s.p.s.a.OpenSamlAuthenticationProvider : Validating SAML response from <IdP>
2020-06-26 10:30:35.102 DEBUG 12544 --- [nio-8080-exec-3] s.s.p.s.a.OpenSamlAuthenticationProvider : Validating 1 assertions
2020-06-26 10:30:35.109 DEBUG 12544 --- [nio-8080-exec-3] s.s.p.s.a.OpenSamlAuthenticationProvider : Found 1 validation errors in SAML response [ID_7e615c61-9dda-4629-90fa-989a817c3282]
2020-06-26 10:30:35.109 DEBUG 12544 --- [nio-8080-exec-3] .p.s.s.f.Saml2WebSsoAuthenticationFilter : Authentication request failed: Saml2AuthenticationException{error=[invalid_assertion] Invalid assertion [ID_573ac246-7ea6-4098-af2a-2c97296011dc] for SAML response [ID_7e615c61-9dda-4629-90fa-989a817c3282]}
org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException: An error occurred while validating the assertion: Unknown Condition '{urn:oasis:names:tc:SAML:2.0:assertion}OneTimeUse' of type 'null' in assertion 'ID_573ac246-7ea6-4098-af2a-2c97296011dc'
at org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.authException(OpenSamlAuthenticationProvider.java:510)
at org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.validateResponse(OpenSamlAuthenticationProvider.java:320)
at org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.authenticate(OpenSamlAuthenticationProvider.java:205)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199)
at org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter.attemptAuthentication(Saml2WebSsoAuthenticationFilter.java:109)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:239)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter.doFilterInternal(Saml2WebSsoAuthenticationRequestFilter.java:146)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92)
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:353)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:223)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:184)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:141)
at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:82)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.springframework.security.saml2.Saml2Exception: An error occurred while validating the assertion: Unknown Condition '{urn:oasis:names:tc:SAML:2.0:assertion}OneTimeUse' of type 'null' in assertion 'ID_573ac246-7ea6-4098-af2a-2c97296011dc'
at org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.validateAssertion(OpenSamlAuthenticationProvider.java:403)
at org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider.validateResponse(OpenSamlAuthenticationProvider.java:317)
... 61 common frames omitted
Sample
I've added a OneTimeUse condition test in a commit to my fork here https://github.com/plnordquist/spring-security/commit/0e5b5a8a34 which replicates this failure.
I think fixing this requires adding a OneTimeUseConditionValidator to the list of ConditionValidators in the OpenSamlAuthenticationProvider but that validator seems to require a ReplayCache that enforces this condition so tokens cannot be replayed.
Comment From: jzheaux
Support for this has been added via the setAssertionValidator configuration method.
To validate assertions that have a <OneTimeUse> condition, you can do:
StorageService storage = new MemoryStorageService();
ReplayCache cache = new ReplayCache();
cache.setStorageService(storage);
ConditionValidator conditionValidator = new OneTimeUseConditionValidator(cache, null);
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
provider.setAssertionValidator(assertionToken -> {
Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
.createDefaultAssertionValidator().context(assertionToken);
Assertion assertion = assertionToken.getAssertion();
OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
ValidationContext context = // ... specify any context parameters needed
try {
if (conditionValidator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
return result;
}
} catch (Exception e) {
return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage());
}
return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
http
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(provider))
);
Note that a default construction of OneTimeUseConditionValidator was not added to OpenSamlAuthenticationProvider at this time since it's not clear to me how to configure its replay cache without needing to replicate significant portions of the OpenSAML API inside of OpenSamlAuthenticationProvider.