I'm using Spring Security on a basic Thymeleaf setup with index.html and login.html, however the default login page always returns 403 Forbidden when the credentials are valid. (It gives a UI error when the credentials don't match, as expected).
I believe it's due to the CSRF token which is already included as a cookie (XSRF-TOKEN) in every request to backend. I'd rather not simply disable CSRF, so I've tried including this token into the POST request in almost every way I could find online:
/login?_csrf=token
<input type="hidden" name="_csrf" ...
into the Thymeleaf form (this is default behaviour and I checked that it does send, but backend rejects it??)Any ideas? What does the default Spring Security /login
POST endpoint expect in the request? How does it expect the CSRF token?
Authentication seems to be working, it's just that CSRF fails on successful login. Or is it something else entirely that I'm missing that's giving me a 403 Forbidden?
Thanks in advance!!
My setup
Spring Boot version: 2.6.2
pom.xml dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zsoltfabok</groupId>
<artifactId>sqlite-dialect</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
index.html
<form th:action="@{/login}" method="post">
<label for="username">Username</label>
<input type="text" name="username" />
<label for="password">Password</label>
<input type="password" name="password" />
<button type="submit">Submit</button>
<!-- there's also a hidden CSRF token generated automatically -->
</form>
WebSecurityConfigurerAdapter
@Configuration
@EnableGlobalAuthentication
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/favicon.ico").anonymous()
.antMatchers("/static/**").anonymous()
.mvcMatchers("/", "/login", "/signup").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.and()
.httpBasic()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
Controller
@Controller
@RequestMapping("/")
public class WebController {
@GetMapping
public String index() {
return "index";
}
}
The issue is your security rules.
.antMatchers("/favicon.ico").anonymous()
.antMatchers("/static/**").anonymous()
.mvcMatchers("/", "/login", "/signup").anonymous()
You configured them for anonymous access. Which means someone who isn't authenticated yet. It will prevent an authenticated user to access any of those URLs (or un-authenticated if you disabled anonymous access).
To fix allow access for anyone using permitAll()
instead of anonymous()
.
.antMatchers("/favicon.ico").permitAll()
.antMatchers("/static/**").permitAll()
.mvcMatchers("/", "/login", "/signup").permitAll()