Issue
I'm trying to update an application to Spring Boot 2.7, which brings Spring Security to 5.7 and deprecates WebSecurityConfigurerAdapter
.
Here's a simplified version of the old code, that creates a single user, and works. Note that this code is used only for local development and testing, using a simple form login:
@Configuration
@Profile("local-auth")
class LocalAuth {
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(
PasswordEncoder passwordEncoder) {
return new WebSecurityConfigurerAdapter() {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("testuser")
.password(passwordEncoder.encode("a"))
.authorities(toAuthorities(List.of("SOME_AUTHORITY")));
}
private List<? extends GrantedAuthority> toAuthorities(List<String> roles) {
return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/login", "/actuator/**", "/publico/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin.defaultSuccessUrl("http://localhost:4200/app/").permitAll())
.logout(logout -> logout.logoutUrl("/logout"))
.csrf(AbstractHttpConfigurer::disable);
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Following the guide at https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter, I changed the code to
@Configuration
@Profile("local-auth")
class LocalAuth {
@Bean
public InMemoryUserDetailsManager userDetailService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("testuser")
.password(passwordEncoder.encode("a"))
.authorities(toAuthorities(List.of("SOME_AUTHORITY")))
.build();
return new InMemoryUserDetailsManager(user);
}
private List<? extends GrantedAuthority> toAuthorities(List<String> roles) {
return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/login", "/actuator/**", "/publico/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin.defaultSuccessUrl("http://localhost:4200/app/").permitAll())
.logout(logout -> logout.logoutUrl("/logout"))
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
The login form is displayed, but when I try to login, I get this error in the login page: No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken. Strangely, nothing gets printed in the application log.
I've also tried to provide an AuthenticationManager
replacing the first bean with
@Bean
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder authBuilder,
PasswordEncoder passwordEncoder) throws Exception {
UserDetails user = User.builder()
.username("govbr.20821922459")
.password(passwordEncoder.encode("a"))
.authorities(toAuthorities(List.of("LU_NVL_CONTA_BASICA")))
.build();
return authBuilder.inMemoryAuthentication().withUser(user).and().build();
}
but then the application fails to start with Cannot apply org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer@313c3cb to already built object.
Does anyone know the proper way to convert the old WebSecurityConfigurerAdapter
?
Edit: Even increasing log level to DEBUG, all I get in a login attempt is
2022-07-27 14:22:26.012 [ ] DEBUG o.s.security.web.FilterChainProxy - Securing POST /login
2022-07-27 14:22:26.013 [ ] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
2022-07-27 14:22:26.015 [ ] DEBUG o.s.security.web.DefaultRedirectStrategy - Redirecting to /svr/rest/login?error
2022-07-27 14:22:26.015 [ ] DEBUG .s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext
2022-07-27 14:22:26.015 [ ] DEBUG .s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext
2022-07-27 14:22:26.015 [ ] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request
2022-07-27 14:22:26.041 [ ] DEBUG o.s.security.web.FilterChainProxy - Securing GET /login?error
2022-07-27 14:22:26.041 [ ] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
2022-07-27 14:22:26.042 [ ] DEBUG .s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext
2022-07-27 14:22:26.042 [ ] DEBUG .s.s.w.c.HttpSessionSecurityContextRepository - Did not store empty SecurityContext
2022-07-27 14:22:26.042 [ ] DEBUG o.s.s.w.c.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request
and the respose to the POST to /login
is a 302 redirect to /login?error
, with no reference to any specific error message or code (unless the error message is determined from the session).
Solution
I found the problem: the code for authentication in production was creating a UserDetailsService
bean as well, so that there were two in the ApplicationContext. This must disable the Spring auto-configuration code that creates whatever is necessary for the in-memory authentication to work.
The solution was to disable the production bean when the local profile is selected. And it seems that's the only possible solution: neither adding @Primary
nor adding @Order
with a higher priority than the prodution bean worked.
Edit: Indeed, configuration is done by the InitializeUserDetailsBeanManagerConfigurer
class, and it checks that there is only one bean of type UserDetailsService
(it's even documented in its Javadoc).
Another possibility is to create an AuthenticationProvider
manually, pointing to the InMemoryUserDetailsManager
. To do that, just register a GlobalAuthenticationConfigurerAdapter
bean (this is a general way to configure the AuthenticationManager
as it is being built):
@Bean
GlobalAuthenticationConfigurerAdapter adapter(InMemoryUserDetailsManager inMemoryUserDetails, PasswordEncoder passwordEncoder) {
return new GlobalAuthenticationConfigurerAdapter() {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(inMemoryUserDetails);
provider.setUserDetailsPasswordService(inMemoryUserDetails);
provider.setPasswordEncoder(passwordEncoder);
auth.authenticationProvider(provider);
}
};
}
Answered By - ekalin
Answer Checked By - Terry (JavaFixing Volunteer)