GPSPRINGSECURITYCORE-70: Remember me functionality fails intermittently with CookieTheftException: Invalid remember-me token
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
Original Reporter: partridge
Environment: app.grails.version=1.3.7 plugins.spring-security-openid=1.0.2
Version: Grails-Spring-Security-Core 1.1.1
Migrated From: http://jira.grails.org/browse/GPSPRINGSECURITYCORE-70
I get a runtime exception using the remember me functionality in the spring security plugin, specifically alngside the openid plugin. I haven't tested whether this is a problem with the core plugin by itself (i.e. remember-me without the openid plugin), but this may be the case.
I was just running through the openid tutorial by Burt, testing out the remember-me functionality with openID, and this error occurs intermittently when closing the browser and re-opening to test the remember-me.
Stacktrace:
```
2011-03-28 20:57:58,404 [http-8080-4] ERROR [/openidtest2].[default] - Servlet.service() for servlet default threw exception
org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
at org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.processAutoLoginCookie(PersistentTokenBasedRememberMeServices.java:90)
at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.autoLogin(AbstractRememberMeServices.java:91)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:77)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:54)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:187)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:187)
at org.codehaus.groovy.grails.plugins.springsecurity.RequestHolderAuthenticationFilter.doFilter(RequestHolderAuthenticationFilter.java:40)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.codehaus.groovy.grails.plugins.springsecurity.MutableLogoutFilter.doFilter(MutableLogoutFilter.java:79)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:79)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:378)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:167)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.servlet.filter.GrailsReloadServletFilter.doFilterInternal(GrailsReloadServletFilter.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:69)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:69)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:849)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:454)
at java.lang.Thread.run(Thread.java:619)
```
After doing more digging, it seems there might be a race condition in the plugin (and/or spring security itself). I extended GormPersistentTokenRepository and overrode updateToken to write some info about the token being persisted, to a file. The following shows the log after I put breakpoints at various points in org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.processAutoLoginCookie() to slow things down, and debugging :
```
// [THREAD] SERIES -> TOKEN (LAST USED DATE)
[http-8080-2] CwiwDT8XoTCD/7zrhyBU8Q== -> ek46OH0IGfDNc/+KecEI0Q== (Sun Mar 27 00:52:58 GMT 2011)
[http-8080-2] CwiwDT8XoTCD/7zrhyBU8Q== -> 4nuEiXh0/AjUdS3LVRLMxg== (Sun Mar 27 00:53:19 GMT 2011)
[http-8080-2] CwiwDT8XoTCD/7zrhyBU8Q== -> TqXbP4SgDMpyMsAsragxvA== (Sun Mar 27 00:53:30 GMT 2011)
[http-8080-3] CwiwDT8XoTCD/7zrhyBU8Q== -> gBxu4GVP3JOH4Ts4djGEDg== (Sun Mar 27 00:54:03 GMT 2011)
[http-8080-2] CwiwDT8XoTCD/7zrhyBU8Q== -> WGslWXP1ccoEt9v9t1QVGQ== (Sun Mar 27 00:53:52 GMT 2011)
// Restart browser ->
// presentedToken: WGslWXP1ccoEt9v9t1QVGQ== PersistentRememberMeToken.tokenValue: gBxu4GVP3JOH4Ts4djGEDg
// -> Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
```
Note how the "last used" times of the last 2 entries before the error are not in the expected order of last used date (without slowing this down with debugging, it is hard to notice the ordering because the times are so close together)? This means the browser is getting one of the tokens in its series-token cookie, and the GormPersistentTokenRepository is persisting the other one, leading to the mismatch the next time the browser is opened. It looks like thread 1 generates a token, then another token, then thread 2 generates a later token, but somehow the 1st thread finishes last. The one that finishes last in order of elapsed time is the one that gets into the browser. The token generated by the 2nd thread with a later last used date stamp ends up the one that is persisted by GormPersistentTokenRepository.
I include a bug report which is my project configured to replicate the bug. Simply run-app, run through the open id linking as per Burt's instructions: http://burtbeckwith.github.com/grails-spring-security-openid/docs/manual/guide/3.%20Tutorials.html#3.1%20User%20registration%20and%20linking
Then enable the remember-me and then continually close browser, open browser at index page, refresh page - repeat this until you get the error - can take several times before it occurs.
Note this bug report project has my MyGormPersistentTokenRepository subclass of GormPersistentTokenRepository used to debug, and is wired in in resources.groovy.
Contributor guide
Research direction
Reproduce the failure using the linked OpenID tutorial flow and the repeated browser close, reopen, and refresh steps. Start with PersistentTokenBasedRememberMeServices.processAutoLoginCookie and the GormPersistentTokenRepository override wired in resources.groovy; compare the persisted token with the browser cookie during concurrent requests. Done means the remember-me flow no longer produces a token mismatch under the reproduction steps.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy, spring
- Domain
- authentication
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100