jhipster / generator-jhipster

JHipster is a development platform to quickly generate, develop, & deploy modern web applications & microservice architectures.
https://www.jhipster.tech
Apache License 2.0
21.45k stars 4.02k forks source link

in production mode cookie theft exception is thrown every time when user's session expires jhipster v3.1.0 #3722

Closed maddy1512 closed 7 years ago

maddy1512 commented 8 years ago

I have been using jhipster v3.1.0 in production mode for my internal system and I am stuck with this particular problem that whenever any user's session expires they are thrown a cookie-theft exception and shown an error page, also after reloading the webpage still same exception is thrown again and again and the users cannot login until they clear their particular cookie. The autologin part some how is not working and below is the stacktrace of the exception that is thrown. This exception only occurs in prod mode.

Forwarding to error page from request [/index.html] due to exception$
org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
        at com.adelement.security.CustomPersistentRememberMeServices.getPersistentToken(CustomPersistentRememberMeServices.java:178)
        at com.adelement.security.CustomPersistentRememberMeServices.processAutoLoginCookie(CustomPersistentRememberMeServices.java:87)
        at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.autoLogin(AbstractRememberMeServices.java:113)
        at sun.reflect.GeneratedMethodAccessor485.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
        at com.sun.proxy.$Proxy152.autoLogin(Unknown Source)
        at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:97)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:48)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:205)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at com.adelement.web.filter.CsrfCookieGeneratorFilter.doFilterInternal(CsrfCookieGeneratorFilter.java:34)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:96)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.springframework.boot.context.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:120)
        at org.springframework.boot.context.web.ErrorPageFilter.access$000(ErrorPageFilter.java:61)
        at org.springframework.boot.context.web.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:95)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.boot.context.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:113)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:240)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:207)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:212)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:141)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:616)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:522)
        at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1095)
        at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:672)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1502)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1458)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)
jdubois commented 8 years ago

Can you also share your .yo-rc.json and node/npm versions ? How are you running in production, are you using CloudFoundry or Heroku ?

maddy1512 commented 8 years ago

I still have no idea why this error occurs when someone is not at all altering any remember-me token. The above stacktrace shows that the exception was fired from the index.html, but sometimes it also shows websocket paths.

Here is my yo-rc.json file:

{
  "generator-jhipster": {
    "baseName": "adelement_admin",
    "packageName": "com.adelement",
    "packageFolder": "com/adelement",
    "authenticationType": "session",
    "hibernateCache": "ehcache",
    "clusteredHttpSession": "no",
    "websocket": "spring-websocket",
    "databaseType": "sql",
    "devDatabaseType": "mysql",
    "prodDatabaseType": "mysql",
    "searchEngine": "no",
    "useSass": false,
    "buildTool": "maven",
    "frontendBuilder": "gulp",
    "enableTranslation": true,
    "enableSocialSignIn": true,
    "rememberMeKey": "89218915288274003bcfb9e7cfdb1c9b54c523f2",
    "testFrameworks": [
      "gatling"
    ],
    "jhipsterVersion": "3.1.0",
    "serverPort": "8080",
    "applicationType": "monolith",
    "jhiPrefix": "jhi",
    "languages": []
  },
  "generator-jhipster-entity-audit": {
    "auditFramework": "custom"
  }
}

I am serving this app from aws using tomcat 8.

jdubois commented 8 years ago

Are you using Elastic Load Balancer in front of your app? I've seen it doesn't work well with Websockets. Are you running multiple instances of your app, with sticky sessions ?

maddy1512 commented 8 years ago

No, we are not using any elastic load balancer in front of our app, and we are running on a single instance. The only thing that we have in front of our app is nginx server as a proxy server.

jdubois commented 8 years ago

Could you try accessing JHipster directly? Just to know if nginx is doing anything wrong.

maddy1512 commented 8 years ago

Steps I followed to recreate the error: 1) go to cookie and alter JESSIONID variable. 2) copy remember-me token and decode it (base64), remeber-me token has two parts combined by a colon ':' so I altered the part that is after the colon and again encoded it to base64. 3) replaced the remember-me token in the cookie with the newly generated token. 4) try to access the web app and it will throw remember-me cookie theft exception and shows you the error page. 5) hit refresh on the error page.

I followed above steps using nginx as well as without nginx and after the fifth step I am redirected to the login page, which is not the issue. But as I said earlier there are times when this error would show up randomly and after hitting refresh still won't redirect to the login page and the only way to access the app would be to delete the cookie.

jdubois commented 8 years ago

Yes I know how to reproduce it - you could also alter the token in the database, which is what I'm usually doing. This is just doing the same thing as if someone stole your token, so no problem with that at all.

Now the real issue is why do you have this issue in production, considering (hopefully!!) that nobody stole your tokens.

I've read your ticket again, is it only occurring with this 3 conditions together:

-> in that case, you should be able to reproduce it consistently on your laptop (without NGinx), can you try this out, and confirm this is causing the issue?

maddy1512 commented 8 years ago

So we again tried to recreate the issue by reducing the session timeout to 1 minute so that the chances of calling autologin increases as a result the exceptions increased and with this exception we had two issues. 1) Randomly this exception was thrown 2) After exception is thrown we are redirected to error page and even after refreshing the page multiple times the app won't redirect us to login page, until and unless we delete the cookie.

Now we found out the reason for the 2nd issue, it was because in the CustomPersistenceRemembermeService class the delete token call was not working. The weird thing was that previous to that call is a getter call to get the token to match the token that is in the cookie which works perfectly fine but delete won't work. So we changed delete(token) to deleteTokenBySeries(series) and added that function to the repository as follows:

@Modifying
    @Transactional
    @Query("DELETE from PersistentToken p where p.series=:series")
    void deleteTokenBySeries(@Param("series") String series);

and this was working fine and after this whenever we would get the exception we need not delete the cookie we we able to reach login page using simple refresh as the previous token would get deleted now.

But still we are not able to catch why the token would change on the first place? Is this because of parallel calls to API?

jdubois commented 8 years ago

I'm sorry but I just tried it again:

What exactly do you mean by the delete token call was not working: do you have stacktrace? Or just the line isn't deleted?

Then regarding your question: of course the token changes each time someone signs in, that's the whole point of having a token, you can only use it once. And if it is re-used, then that means someone stole your cookie and used it, so that's really bad.

Can you try without NGinx? I'm pretty sure it's your NGinx configuration which is doing something wrong, like mixing up the cookies or session IDs...

jdubois commented 8 years ago

I'm closing this for lack of information, I guess the issue comes from NGinx as this can't be reproduced locally.

smlshn commented 7 years ago

I have the same issue and here is my log

2016-12-06 20:01:57.558  WARN 9462 --- [nio-8080-exec-4] .s.ChangeSessionIdAuthenticationStrategy : Your servlet container did not change the session ID when a new session was created. You will not be adequately protected against session-fixation attacks
2016-12-06 21:10:32.652  INFO 9462 --- [nio-8080-exec-2] c.m.s.CustomPersistentRememberMeServices : presentedToken=A8HrkdCMA153qAhKPocmTw== / tokenValue=A8HrkdCMA153qAhKPocmTw==
2016-12-07 08:31:41.072  INFO 9462 --- [nio-8080-exec-1] c.m.s.CustomPersistentRememberMeServices : presentedToken=RXbuRFuYWrgbq6WvTMYCWQ== / tokenValue=RXbuRFuYWrgbq6WvTMYCWQ==
2016-12-07 08:31:41.225  INFO 9462 --- [nio-8080-exec-9] c.m.s.CustomPersistentRememberMeServices : presentedToken=RXbuRFuYWrgbq6WvTMYCWQ== / tokenValue=RRSiHp2tLX6KxZHpmvKDzw==
2016-12-07 08:31:41.247 ERROR 9462 --- [nio-8080-exec-9] o.s.boot.context.web.ErrorPageFilter     : Forwarding to error page from request [/index.html] due to exception [Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.]

org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
    at tr.com.mt.security.CustomPersistentRememberMeServices.getPersistentToken(CustomPersistentRememberMeServices.java:175) ~[classes/:0.0.1-SNAPSHOT]
    at tr.com.mt.security.CustomPersistentRememberMeServices.processAutoLoginCookie(CustomPersistentRememberMeServices.java:85) ~[classes/:0.0.1-SNAPSHOT]
    at org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.autoLogin(AbstractRememberMeServices.java:113) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_101]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_101]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_101]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_101]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) ~[spring-aop-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at com.sun.proxy.$Proxy151.autoLogin(Unknown Source) ~[na:na]
    at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:97) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:169) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:48) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:133) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:205) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:120) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at tr.com.mt.web.filter.CsrfCookieGeneratorFilter.doFilterInternal(CsrfCookieGeneratorFilter.java:34) ~[classes/:0.0.1-SNAPSHOT]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:96) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:91) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:53) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:213) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:176) ~[spring-security-web-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:121) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.springframework.boot.context.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:120) [spring-boot-1.3.1.RELEASE.jar:1.3.1.RELEASE]
    at org.springframework.boot.context.web.ErrorPageFilter.access$000(ErrorPageFilter.java:61) [spring-boot-1.3.1.RELEASE.jar:1.3.1.RELEASE]
    at org.springframework.boot.context.web.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:95) [spring-boot-1.3.1.RELEASE.jar:1.3.1.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.boot.context.web.ErrorPageFilter.doFilter(ErrorPageFilter.java:113) [spring-boot-1.3.1.RELEASE.jar:1.3.1.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192) [catalina.jar:8.5.4]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165) [catalina.jar:8.5.4]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) [catalina.jar:8.5.4]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:108) [catalina.jar:8.5.4]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:522) [catalina.jar:8.5.4]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [catalina.jar:8.5.4]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) [catalina.jar:8.5.4]
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620) [catalina.jar:8.5.4]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [catalina.jar:8.5.4]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:349) [catalina.jar:8.5.4]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:1110) [tomcat-coyote.jar:8.5.4]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-coyote.jar:8.5.4]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:785) [tomcat-coyote.jar:8.5.4]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1425) [tomcat-coyote.jar:8.5.4]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-coyote.jar:8.5.4]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_101]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_101]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-util.jar:8.5.4]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_101]
indrekru commented 7 years ago

Also getting it. Obviously you have some other setup procedure on your environment, which most developers don't go through. And still a lot of people fall into the same trap, looking at the same topic being discussed throughout the years.

I can describe my situation: No nginx, no hazelcast, just a spring boot application. I run my app in a "screen" simply with "java -jar myapp.jar" straight from the tomcat, no proxies. When I kill the app and start it again, I get this nasty exception, throwing a 500 page. This all happens in "production", it never happened in localhost.

I read from somewhere that there's some race-condition happening with fetching the token value, first thread - changes value, second thread - gets wrong value - Exception. Can it be fixed by making it thread-safe? I added ignores to my security config to not apply security filters on fetching file resources. No use.

Perhaps you can describe you typical app setup in some simple app code, cause we obviously can't give you our code.

bluegaspode commented 7 years ago

I analyzed to problem (and all logs) a bit further and can provide the following details to reproduce the problem with a very high probability (without hacks every 3-5th time on a basic JHipster app, with hacks even more).

I'm also facing this error regularly on my local dev machine and also can find traces of it in production mode (with a small number of fixed beta-testers - I don't expect real cookie thefts there).

Starting with High-Level details (followed by detailed instructions and logs)

Here some more details on how I can reproduce the bug:

On the logs you can now see consecutive parallel calls from different thread for a successful authentication, here are the most important lines from my invocation triggering the error

2017-04-23 20:44:51.549 DEBUG 33383 --- [ XNIO-19 task-2] b.k.s.CustomPersistentRememberMeServices : Remember-me cookie detected
2017-04-23 20:44:51.549 DEBUG 33383 --- [ XNIO-19 task-3] b.k.s.CustomPersistentRememberMeServices : Remember-me cookie detected
2017-04-23 20:44:51.558  INFO 33383 --- [ XNIO-19 task-3] b.k.s.CustomPersistentRememberMeServices : presentedToken=U6j2kZnhhY6PrK6gpYcSDQ==:DXAKeA2+Wc7WsPJIgYpppA== / tokenValue=DXAKeA2+Wc7WsPJIgYpppA==
2017-04-23 20:44:51.558  INFO 33383 --- [ XNIO-19 task-2] b.k.s.CustomPersistentRememberMeServices : presentedToken=U6j2kZnhhY6PrK6gpYcSDQ==:DXAKeA2+Wc7WsPJIgYpppA== / tokenValue=DXAKeA2+Wc7WsPJIgYpppA==
2017-04-23 20:44:51.558 DEBUG 33383 --- [ XNIO-19 task-2] b.k.s.CustomPersistentRememberMeServices : Refreshing persistent login token for user 'admin', series 'U6j2kZnhhY6PrK6gpYcSDQ=='
2017-04-23 20:44:51.558 DEBUG 33383 --- [ XNIO-19 task-3] b.k.s.CustomPersistentRememberMeServices : Refreshing persistent login token for user 'admin', series 'U6j2kZnhhY6PrK6gpYcSDQ=='
2017-04-23 20:44:51.569  INFO 33383 --- [ XNIO-19 task-3] b.k.s.CustomPersistentRememberMeServices : new tokenValue=U6j2kZnhhY6PrK6gpYcSDQ==:B8wXDNczh9qIMkkfGKY3aQ==
2017-04-23 20:44:51.571 DEBUG 33383 --- [ XNIO-19 task-3] d.b.knxslp.security.UserDetailsService   : Authenticating admin
2017-04-23 20:44:51.571  INFO 33383 --- [ XNIO-19 task-2] b.k.s.CustomPersistentRememberMeServices : new tokenValue=U6j2kZnhhY6PrK6gpYcSDQ==:NXiEaMxrr9Os2EvLGBJElg==
2017-04-23 20:44:51.571 DEBUG 33383 --- [ XNIO-19 task-2] d.b.knxslp.security.UserDetailsService   : Authenticating admin
2017-04-23 20:44:51.989 DEBUG 33383 --- [ XNIO-19 task-3] d.b.knxslp.aop.logging.LoggingAspect     : Enter: de.bluegaspode.knxslp.web.rest.SurveyResource.getSurvey() with argument[s] = [1220]
2017-04-23 20:44:51.996 DEBUG 33383 --- [ XNIO-19 task-2] d.b.knxslp.aop.logging.LoggingAspect     : Enter: de.bluegaspode.knxslp.service.UserService.getUserWithAuthorities() with argument[s] = []
2017-04-23 20:44:52.010 DEBUG 33383 --- [ XNIO-19 task-2] d.b.knxslp.aop.logging.LoggingAspect     : Exit: de.bluegaspode.knxslp.web.rest.AccountResource.getAccount() with result = <200 OK,UserDTO{login='admin', firstName='Administrator', lastName='Administrator', email='admin@localhost', activated=true, langKey='en', authorities=[ROLE_USER, ROLE_ADMIN]},{}>
2017-04-23 20:44:52.013 DEBUG 33383 --- [ XNIO-19 task-3] d.b.knxslp.aop.logging.LoggingAspect     : Exit: de.bluegaspode.knxslp.web.rest.SurveyResource.getSurvey() 

(the "new tokenValue" line is an added debug message in CustomPersistentRememberMeServices.addCookie() )

summary of the logs: due to the parallel calls/thread the current "rememberme" cookie was validated twice and two new tokens were created. The token was first created on thread "Task3" and then recreated on "Task2". Unfortunately service calls had different timings so "Task3" is finishing later - browser will get the (already invalid) rememberme-token from Task3.

This behaviour obviously doesn't happen always, but give it enough tries (or add random delays to all involved Services) to increase chances of seeing it.

Now we have the base to see the actual problem:

now we get the actual error, when the browser doesn't present the latest token:

2017-04-23 20:47:37.477  INFO 33383 --- [ XNIO-20 task-2] b.k.s.CustomPersistentRememberMeServices : presentedToken=U6j2kZnhhY6PrK6gpYcSDQ==:B8wXDNczh9qIMkkfGKY3aQ== / tokenValue=NXiEaMxrr9Os2EvLGBJElg==
2017-04-23 20:47:37.483 DEBUG 33383 --- [ XNIO-20 task-2] b.k.s.CustomPersistentRememberMeServices : Cancelling cookie
2017-04-23 20:47:37.483 ERROR 33383 --- [ XNIO-20 task-3] io.undertow.request                      : UT005023: Exception handling request to /api/surveys/1220

org.springframework.security.web.authentication.rememberme.CookieTheftException: Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.

You can see in the log message, that my browser decided to use the 'Task3' token, while the 'Task2' Token was the current one.

In production of course we don't have as many server restarts (hopefully). But the same scenario will also happen, when sessions timeout.

bluegaspode commented 7 years ago

Thinking more about the bug, right now I'm not sure how this can be 'easily' fixed.

Simple synchronisation will not help, as one never knows how long the actual service calls take and which cookie the browser will see last.

Keeping more than one token "valid" defeats the purpose of the CookieTheft measures.

Idea1: some sort of an AroundAdvice. I.e. the CustomPersistentRememberMeServices detects that a Token Refresh is needed, but the actual refresh will only happen after (!) the actual service call (and synchronized).

Idea2: processAutoLoginCookie will get synchronized. It will not always refresh the token, but sets the 'current' token as a cookie, when its only some minutes old. So only the first thread will generate a new token, but all threads will return the same new token. Thus it won't matter which thread later on returns how fast.

bluegaspode commented 7 years ago

This (unresolved) Stackoverflow covers the same topic http://stackoverflow.com/questions/13613526/how-to-combine-a-persistent-login-cookie-with-parallel-ajax-requests/

and leads to a solution proposal on the Drupal CMS 7 years ago (https://www.drupal.org/node/327263#comment-3428038) which was later implemented with a caching table (https://www.drupal.org/node/327263#comment-3659274).

Most annoying thing is the needed house-keeping of the database-table to remove old entries.

jdubois commented 7 years ago

Thank you so much @bluegaspode for analysing this!! This is a very tricky issue, I already spent a lot of time looking for it. Anyway, it looks like there isn't a good solution here. Maybe we should just stop using session-based authentication? At the moment, my statistics show it has dropped from 5% usage in februrary to only 2%... Everybody seems to be using JWT, and focusing on it would help us improve it.

bluegaspode commented 7 years ago

How would switching to JWT solve the "remember-me" issue (that the CustomPersistenceRemembermeService) is about?

The main reason for the current implementation is to detect cookie theft, i.e.:

On the other hand, articles like these https://stormpath.com/blog/build-secure-user-interfaces-using-jwts

claim that JWT cannot be stolen in the first place when a) only https is used b) jwt is stored as a (http-only) cookie c) csrf countermeasures are taken.

JHipster can provide (b)+(c), enforcing (a) is a bit harder? Well on the other hand: when not using https already the first login with username+password is flawed.

So given all that we could also keep the current remember-me approach and just remove the "attack detection". As its guaranteed that the current "remember-me"-token is only valid for 30days, and maybe also already cannot be stolen (needs to be checked, if it is a secure cookie already), it is as good as JWT?

Cheers Stefan

deepu105 commented 7 years ago

I also would prefer to keep session auth its tje simplest and most useful for simple monoliths.

On Mon, 24 Apr 2017, 2:26 pm bluegaspode, notifications@github.com wrote:

How would switching to JWT solve the "remember-me" issue (that the CustomPersistenceRemembermeService) is about?

The main reason for the current implementation is to detect cookie theft, i.e.:

  • user logs in, gets a long lived "remember-me" token
  • attacker steals the token, can use it to login
  • user logs in again - attack is detected - all tokens issued so far are invalidated automatically, a real login is enforced

On the other hand, articles like these https://stormpath.com/blog/build-secure-user-interfaces-using-jwts

claim that JWT cannot be stolen in the first place when a) only https is used b) jwt is stored a cookie c) csrf countermeasures are taken.

JHipster can provide (b)+(c), enforcing (a) is a bit harder? Well on the other hand: when not using https already the first login with username+password is flawed.

So given all that we could also keep the current remember-me approach and just remove the "attack detection". As its guaranteed that the current "remember-me"-token is only valid for 30days, and maybe also already cannot be stolen (needs to be checked, if it is a secure cookie already), it is as good as JWT?

Cheers Stefan

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/jhipster/generator-jhipster/issues/3722#issuecomment-296647073, or mute the thread https://github.com/notifications/unsubscribe-auth/ABDlFxs12SdzCnMkcndhgX9AzImUHCw8ks5rzJTlgaJpZM4I3CEp .

bluegaspode commented 7 years ago

I created a patch to CustomPersistentRememberMeServices to solve the problem in my local JHipster application (see attachment). I'm afraid though I don't know enough of JHipsters internal workings to create a jhipster-generator patch, but hope you are still able to review the changes and see if it can help (or inspire) you.

patch.diff.zip CustomPersistentRememberMeServices.java.zip

jdubois commented 7 years ago

I need to look at that, but this looks like the best strategy at the moment. Of course it won't work well in a distributed environment, but people using Session-based authentication are supposed to use sticky sessions with a load balancer, anyway.

jdubois commented 7 years ago

This looks good to me, I'm going to put it in our template and test

jdubois commented 7 years ago

Sorry, I didn't see the huge synchronized block. This is effectively a big issue.

My idea for the moment is to take your implementation, but remove the synchronized block: this is "optimized locking" like we do quite often in databases, so the bug is still here, but should be quite reduced. Hopefully this should be good enough for most people, and won't have the performance penalty. At least that's a first solution, that we can roll out without taking much risk.

bluegaspode commented 7 years ago

I don't think, that the solution will work without the synchronized block.

If you have two or more concurrent requests (only then the problem actually turns up), none of them will hit the 'upgradedTokenCache' when not synchronized. The upgradedTokenCache is only populated after a database read and database write, this is tons of time for concurrent threads to NOT hit the cache.

Do keep in mind:

jdubois commented 7 years ago

Yes @bluegaspode you are totally right - I'm sorry, I should have written this up here, but I had second thoughts, and what I did is clearly not good enough. I'm reopenning this so I don't forget about it - not sure what I'll do for the next release, as I don't have the time for this

bluegaspode commented 7 years ago

When its not good enough (yet), then clean code is better than the added complexity of the last change I guess. I'd suggest reverting the last change, when the next release is due - and later on having a clean start when there is more time.

Personally I'm using the patch in the generated application as my user count permits not beeing afraid of the synchronized block. Anyone else finding this issue can also apply the patch locally and help himself.

jdubois commented 7 years ago

The "synchronized" block isn't so bad:

And anyway, this is only for people using session-based authent:

jdubois commented 7 years ago

Let's go for the "synchronized" block -> I'll code this and close the ticket with my commit

CatchSandeepVaid commented 7 years ago

Just wondering how you integrated your custom PersistentTokenRememberMeServices into Spring as inside RememberMeConfigurer.createPersistentRememberMeServices() org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices instance is created, so how will you integrate your custom PersistentTokenRememberMeServices instead of this instance?

private AbstractRememberMeServices createPersistentRememberMeServices(H http, String key) { UserDetailsService userDetailsService = getUserDetailsService(http); return new PersistentTokenBasedRememberMeServices(key, userDetailsService, this.tokenRepository); }