Describe the bug Configuration rules that worked in Spring Security 5 don't work in 6.0.1.
After migrating the security configuration to Spring Security 6.0.1, if we use a bad credentials(username/password) then a browser gets stuck, Hibernate runs a query endlessly and the control does not redirect to the login page. The project uses Spring MVC and ThymeLeaf.
To migrate from Spring Security 5 (Spring Boot 2.1.3.RELEASE) to Spring Security 6.0.1 (Spring Boot 3.0.2) I changed SecurityConfiguration.java file from
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers("/","/h2-console/**").permitAll()
.antMatchers("/admin").access("hasAuthority('ADMIN')")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login").permitAll()
.and()
.httpBasic();
http
.csrf().disable();
http
.headers().frameOptions().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsServiceBean())
.passwordEncoder(passwordEncoder());
}
to
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/h2-console/**")
.permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.failureUrl("/login?error=true")
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll())
.httpBasic(Customizer.withDefaults());
http
.csrf().disable();
http
.headers().frameOptions().disable();
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
}
and I changed SSUserDetailsService.java file from
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
User appUser = userRepository.findByUsername(username);
if(appUser == null){
System.out.println("User not found with the provided username" + appUser.toString());
return null;
}
System.out.println("User from username " + appUser.toString());
return new org.springframework.security.core.userdetails.User(
appUser.getUsername(),
appUser.getPassword(),
getAuthorities(appUser));
} catch (Exception e){
throw new UsernameNotFoundException("User not found");
}
}
private Set<GrantedAuthority> getAuthorities(User appUser) {
Set<GrantedAuthority> authorities = new HashSet<>();
for(Role role: appUser.getRoles()){
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRole());
authorities.add(grantedAuthority);
}
System.out.println("User authorities are" + authorities.toString());
return authorities;
}
to
@Override
public UserDetails loadUserByUsername(String username){
try {
User appUser = userRepository.findByUsername(username);
if (appUser == null) {
System.out.println("User not found with the provided username" + appUser.toString());
return null;
}
System.out.println("User from username " + appUser.getUsername());
return org.springframework.security.core.userdetails.User
.withUsername(appUser.getUsername())
.password(appUser.getPassword())
.roles(getAuthorities(appUser))
.build();
} catch (Exception e) {
throw new UsernameNotFoundException("User not found");
}
}
private String[] getAuthorities(User appUser) {
var authorities = new HashSet<String>();
for (var role : appUser.getRoles()) {
var grantedAuthority = new SimpleGrantedAuthority(role.getRole());
authorities.add(grantedAuthority.getAuthority());
}
System.out.println("User authorities are " + authorities);
return Arrays.copyOf(authorities.toArray(),authorities.size(), String[].class);
}
To Reproduce Steps to reproduce error behavior (springboot_3.0 branch contains an example with Spring Security 6.0.1 (Spring Boot 3.0.2)):
- git clone -b springboot_3.0 https://github.com/mhussainshah1/SpringBoot_404.git
- cd SpringBoot_404
- mvn clean package
- mvn spring-boot:run
- Open the browser with link http://localhost:8080/
- Login with user/password (user is admin, password is password)
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select r1_0.user_id,r1_1.id,r1_1.role from user_data_roles r1_0 join role r1_1 on r1_1.id=r1_0.role_id where r1_0.user_id=?
User from username admin
User authorities are [ADMIN]
- Login with bad credential (user is dave, password is begreat)
The browser will stuck and Hibernate query run endlessly until stopped by command CTRL + C
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Hibernate: select u1_0.id,u1_0.email,u1_0.enabled,u1_0.first_name,u1_0.last_name,u1_0.password,u1_0.username from user_data u1_0 where u1_0.username=?
Expected behavior
After putting in bad credentials (username or password) the login page should appear with the following text
Invalid username or password
Steps to reproduce success behavior (main branch contains an example with Spring Security 5 (Spring Boot 2.1.3.RELEASE)):
- git clone https://github.com/mhussainshah1/SpringBoot_404.git
- cd SpringBoot_404
- mvn clean package
- mvn spring-boot:run
- Open browser with link http://localhost:8080/
- Login page will open successfully
-
Log in with user/password (user is admin, password is password) The home page will open at the link http://localhost:8080/
-
Login with bad credential (user is dave, password is begreat) It should redirect to Login Page and show as follow
Sample
A link to a GitHub repository with a minimal, reproducible sample. (see springboot_3.0 branch).
Comment From: ashutosh049
Hi.
Have you tried providing an AuthneticationEntryPoint ?
private final MyUserDetailsService myUserDetailsService;
private final AppDefaultAuthenticationFailedEntryPoint appDefaultAuthenticationFailedEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/h2-console/**")
.permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.failureUrl("/login?error=true")
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll())
// .httpBasic(Customizer.withDefaults());
.httpBasic(httpBasicConfigurer_ -> {
httpBasicConfigurer_.realmName("SOME-REALM");
httpBasicConfigurer_.authenticationEntryPoint(appDefaultAuthenticationFailedEntryPoint);
})
http
.csrf().disable();
http
.headers().frameOptions().disable();
return http.build();
}
AppDefaultAuthenticationFailedEntryPoint.java class
@Slf4j
@RequiredArgsConstructor
@Component
public class AppDefaultAuthenticationFailedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
HttpStatus status = HttpStatus.UNAUTHORIZED;
log.error("Failed Authentication: {}", authException.getMessage(), authException);
response.setStatus(status.value());
response.sendError(status.value(), "Unauthorized Access");
}
}
Comment From: sirDarey
Nice Article. However, the method, UserDetails loadUserByUsername is not allowed to return null; You must return UsernameNotFoundException.
Comment From: sjohnr
Hi @mhussainshah1, I believe @sirDarey is correct.