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.
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 aJSESSIONID
andXSRF-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.
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 newXSRF-TOKEN
. This results in a mismatch causing a403
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 preceding200 get user
to return the newXSRF-TOKEN
also?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 theXSRF_TOKEN
cookie being updated properly, while the same correctJSESSIONID
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 theXSRF-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:
the OncePerRequestFilter,
the CsrfTokenRepository,
the GlobalAuthenticationConfigurerAdapter and/or
the Principal returned by the
/user
service.
But what specific changes need to be made to solve the problem?
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
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 whencsrfTokenRepository
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:Then you can configure it: