Spring Security + Thymeleaf - TemplateInputException after a second login

Viewed 459

I have a navbar with LOGIN, PROFILE, and LOGOUT. My goal is to remove the LOGIN and show only PROFILE and LOGOUT after the user is logged in. I have the controller checking for authenticated users:

@Controller
public class LoginController {

    @GetMapping("/login")
    private String loginRender(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken){
            return "login";
        }else {
            return "redirect:/";
        }
    }
}

The Thymeleaf template looks like this:

<ul class="logout-ul top-links-container">
    <li class="top-links-item  text-center" sec:authorize="!isAuthenticated()"><a href="/login"> <i class="icon-user4" style="color: #e35f5f"></i>Login</a></li>
    <li class="top-links-item text-center" sec:authorize="isAuthenticated()"><a href="/user/profile"><i class="icon-user4" style="color: #e35f5f"></i>Profile</a></li>
    <li class="text-center logout" sec:authorize="isAuthenticated()"><form th:action="@{/logout}" th:method="post"><button title="LOGOUT" class="btn" type="submit"><i class="icon-off"></i></button></form></li>
</ul>

Configuration:

 http
            .authorizeRequests()
            .antMatchers("/login", "/", "register",
                    "/custom-transfers/**", "/about-us", "/information/**", "/support/**").permitAll()
            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
            .antMatchers("/admin").hasRole(UserRoleEnum.ADMIN.name())
            .antMatchers("/**").authenticated()
          .and()
                .formLogin()
                .loginPage("/login")
                .usernameParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY)
                .passwordParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY)
                .defaultSuccessUrl("/user/profile")
                //TODO validation page
                .failureForwardUrl("/login")
          .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID");

ISSUE It all works when the user first logs in. When logouts and log in again, Thymleaf shows a TemplateInputException.

Exception:

ERROR 26304 --- [nio-8080-exec-9] s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request [/login] and exception [An error happened during template parsing (template: "class path resource [templates/login.html]")] as the response has already been committed. As a result, the response may have the wrong status code.

org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/login.html]")
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:241) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parseStandalone(AbstractMarkupTemplateParser.java:100) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:666) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072) ~[thymeleaf-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.spring5.view.ThymeleafView.renderFragment(ThymeleafView.java:366) ~[thymeleaf-spring5-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.thymeleaf.spring5.view.ThymeleafView.render(ThymeleafView.java:190) ~[thymeleaf-spring5-3.0.12.RELEASE.jar:3.0.12.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1400) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1145) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1084) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.53.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.10.jar:5.3.10]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.53.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.53.jar:9.0.53]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:327) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:121) ~[spring-security-web-5.5.2.jar:5.5.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:115) ~[spring-security-web-5.5.2.jar:5.5.2]

I tried placing sec:authorize="!isAuthenticated()" outside in a <div>, tried to pass a boolean to the template with a RedirectAttribute and Model, but I can't still fix the issue. It works when I add: <li class="top-links-item" sec:authorize="!isAuthenticated()">TEST</li> so I assume is because I use sec:authorize for a login page, which is managed by Spring Security. Could there be any relation?

UPDATE I found that after logout the Cookie destroys and it doesn't creates a new one for a second login. Also the issue is only of logout form in the html when I add sec:authorize=isAuthenticated(). Therefore the problem is connected to the logout instead of the login page. Why could this happen?

This occurs only when I use isAuthenticated() in Thyemleaf.

What am I doing wrong? I would really appreciate your help. Thank you.

1 Answers

I think that what happens here is the following:

Your logout form is managed by Thymeleaf which means that under the hood Thymeleaf will try to create a hidden field with the CSRF token inside. This normally works and it requires existing or new session.

The problem is that Thymeleaf is designed to minimize latencies by producing template output as your template is being processed. Thus when a page is longer in size (as it is in your case) by the time when the engine reaches your logout <form> tag some of the output MAY HAVE BEEN streamed to the client and the server starts to consider your response as committed. Once the response is marked as committed Thymeleaf will fail to access/create a session. I guess this is an architectural constraint.

So I'm not sure what exactly to advise you here. Maybe try to output the logout button earlier in your templates or optionally try to eagerly initialize the CSRF token. This setting in HttpSecurity will possibly do it:

.and()
   .csrf()
   .csrfTokenRepository(new HttpSessionCsrfTokenRepository())
Related