How do I force Spring Security to update XSRF-TOKE

2019-05-29 19:42发布

A REST Spring Security /user service in a Spring Boot application is failing to immediately update the XSRF-TOKEN cookie when a user authenticates. This is causing the next request for /any-other-REST-service-url to return an Invalid CSRF certificate error, until the /user service is called again. How can this problem be resolved so that the REST /user service properly updates the XSRF-TOKEN cookie in the same request/response transaction in whichit first authenticates the user?

The backend REST /user service is called three times by a front end app, but the /user service only returns matched JSESSIONID/XSRF-TOKEN cookies on the first and third call, NOT on the second call.

  1. In the first request to the server, no credentials (no username or password) are sent to the / url pattern, which I think calls the /user service, and the server responds with a JSESSIONID and XSRF-TOKEN that it associated with an anonymous user. The Network tab of the FireFox developer tools shows these cookies as:

    Response cookies:  
    JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
        path:"/"
        httpOnly:true
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
        path:"/"
    

    The user then makes various requests for publicly accessible resources without error, and the Network tab of the FireFox developer tools shows these same cookie values.

  2. The second request to the /user service is done though a login form, which sends a valid username and password, which the/user service uses to authenticate the user. But the /user service only returns an updated jsessionid cookie, and does not update the xsrf-token cookie in this step. Here are the cookies shown in the Network tab of the FireFox developer tools at this point:

    The 200 GET user included the following cookies in the Network tab of FireFox:

    Response cookies:  
    JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
        path:"/"
        httpOnly:true
    AUTH1:"yes"
    
    Request cookies:
    JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
    

    Note that the response included a new JSESSIONID, but did not include a new XSRF-TOKEN. This results in a mismatch causing a 403 error (due to invalid csrf token) in the subsequent requests to other rest services, until this is resolved by a third call to the /user service. is there a way that we can force the preceding 200 get user to return the new XSRF-TOKEN also?

  3. The third call to the backend REST /user service uses the very same username and password credentials that were used in the second request shown above, but this third call to /user results in the XSRF_TOKEN cookie being updated properly, while the same correct JSESSIONID is retained. Here is what the Network tab of the FireFox developer tools shows at this point:

    The 200 GET user shows that the mismatched request forces an update of the XSRF-TOKEN in the response:

    Response cookies:
    XSRF-TOKEN:"ca6e869c-6be2-42df-b7f3-c1dcfbdb0ac7"
        path:"/"
    AUTH1:"yes"
    
    Request cookies:  
    JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
    XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
    

The updated xsrf-token now matches the jsessionid, and thus subsequent requests to other backend rest services can now succeed.

What specific changes can be made to the code below to force an update of both the XSRF-TOKEN and the JSESSIONID cookies the first time the /user service is called with proper username and password by the login form? Do we make specific changes in the code for the backend /user method in Spring? Or is the change made in the Security Configuration classes? What can we try to fix this problem?

The code for the backend /user service and the Security Config are in the main application class of the Spring Boot backend app, which is in UiApplication.java as follows:

@SpringBootApplication
@Controller
@EnableJpaRepositories(basePackages = "demo", considerNestedRepositories = true)
public class UiApplication extends WebMvcConfigurerAdapter {

    @Autowired
    private Users users;

    @RequestMapping(value = "/{[path:[^\\.]*}")
    public String redirect() {
        // Forward to home page so that route is preserved.
        return "forward:/";
    }

    @RequestMapping("/user")
    @ResponseBody
    public Principal user(HttpServletResponse response, HttpSession session, Principal user) {
        response.addCookie(new Cookie("AUTH1", "yes"));
        return user;
    }

    public static void main(String[] args) {
        SpringApplication.run(UiApplication.class, args);
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver slr = new SessionLocaleResolver();
        slr.setDefaultLocale(Locale.US);
        return slr;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Configuration
    protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {

        @Autowired
        private Users users;

        @Override
        public void init(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(users);
        }
    }

    @SuppressWarnings("deprecation")
    @Configuration
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    @EnableWebMvcSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.httpBasic().and().authorizeRequests()
                .antMatchers("/registration-form").permitAll()
                .antMatchers("/confirm-email**").permitAll()
                .antMatchers("/submit-phone").permitAll()
                .antMatchers("/check-pin").permitAll()
                .antMatchers("/send-pin").permitAll()
                .antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*") 
                .permitAll().anyRequest().authenticated().and().csrf()
                .csrfTokenRepository(csrfTokenRepository()).and()
                .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
        }

        private Filter csrfHeaderFilter() {
            return new OncePerRequestFilter() {
                @Override
                protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

                    CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
                    if (csrf != null) {
                        Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
                        String token = csrf.getToken();
                        if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                            cookie = new Cookie("XSRF-TOKEN", token);
                            cookie.setPath("/");
                            response.addCookie(cookie);
                        }
                    }
                    filterChain.doFilter(request, response);
                }
            };
        }

        private CsrfTokenRepository csrfTokenRepository() {
            HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
            repository.setHeaderName("X-XSRF-TOKEN");
            return repository;
        }

    }

}

The relevant segment of the server logs showing the CSRF error is:

2016-01-20 02:02:06.811 DEBUG 3995 --- [nio-9000-exec-5] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@70b8c8bb
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy        : /send-pin at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for http://localhost:9000/send-pin

What specific changes do I need to make to the code above to resolve this CSRF error?

How do I force an immediate update of the XSRF cookie upon whenever the backend /user service changes a user's status (login, logout, etc.)?

Note: I am guessing (based on my research) that the solution to this problem will involve changing the configuration of some combination of the following Spring Security classes, all of which are defined in the UiApplication.java shown below:

  1. the WebSecurityConfigurerAdapter,

  2. the OncePerRequestFilter,

  3. the CsrfTokenRepository,

  4. the GlobalAuthenticationConfigurerAdapter and/or

  5. the Principal returned by the /user service.

But what specific changes need to be made to solve the problem?

1条回答
Deceive 欺骗
2楼-- · 2019-05-29 19:53

Updated Answer

The reason you are getting a 401 is because a basic authentication header is found in the request when the user is registering. This means Spring Security tries to validate the credentials but the user is not yet present so it responds with a 401.

You should

  • Make the /register endpoint public and provide a controller that registers the user
  • Do not include the username/password for registration form in the Authorization header as this will cause Spring Security to try to validate the credentials. Instead include the parameters as JSON or form encoded parameters that your /register controller process

Original Answer

After authenticating, Spring Security uses CsrfAuthenticationStrategy to invalidate any CsrfToken's (to ensure that a session fixation attack is not possible). This is what triggers a new CsrfToken to be used.

However, the problem is that csrfTokenRepository is invoked before authentication is performed. This means that when csrfTokenRepository checks to see if the token has changed the result if false (it has not changed yet).

To resolve the issue, you can inject a custom AuthenticationSuccessHandler. For example:

public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    public void onAuthenticationSuccess(HttpServletRequest request,
                HttpServletResponse response, Authentication authentication)
                throws ServletException, IOException {
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrf != null) {
            Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
            String token = csrf.getToken();
            if (cookie == null || token != null && !token.equals(cookie.getValue())) {
                cookie = new Cookie("XSRF-TOKEN", token);
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        }
        super.onAuthenticationSuccess(request,response,authentication);
    }
}

Then you can configure it:

    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()
                .successHandler(new MyAuthenticationSuccessHandler())
                .and()
            .httpBasic().and()
            .authorizeRequests()
                .antMatchers("/registration-form").permitAll()
                .antMatchers("/confirm-email**").permitAll()
                .antMatchers("/submit-phone").permitAll()
                .antMatchers("/check-pin").permitAll()
                .antMatchers("/send-pin").permitAll()
                .antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*").permitAll()
                .anyRequest().authenticated()
                .and()
            .csrf()
                .csrfTokenRepository(csrfTokenRepository())
                .and()
            .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
    }
查看更多
登录 后发表回答