I am trying to use the new Component-Based (Without WebSecurityConfigurerAdapter) configuration and setted up my Security Configuration as follow:
SecurityConfiguration.java file
package com.tito.userservice.security; import com.tito.userservice.filter.CustomAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfiguration { private final UserDetailsService userDetailsService; private final BCryptPasswordEncoder bCryptPasswordEncoder; private final AuthenticationManagerBuilder authManagerBuilder; @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } //----------------HTTP SECURITY CONFIG------------------------------------- @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable(); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .authorizeRequests().anyRequest().permitAll(); http.addFilter(new CustomAuthenticationFilter(authManagerBuilder.getOrBuild())); return http.build(); } //----------------HTTP SECURITY CONFIG------------------------------------- }
but when I run the application and try to log a user the following error appears even though I have the BCryptPasswordEncoder as a bean in my SecurityConfiguration:
Error Stack Trace*
2022-08-16 21:02:04.220 ERROR 13112 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:289) ~[spring-security-crypto-5.7.2.jar:5.7.2] at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:237) ~[spring-security-crypto-5.7.2.jar:5.7.2] at org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:76) ~[spring-security-core-5.7.2.jar:5.7.2] at org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:147) ~[spring-security-core-5.7.2.jar:5.7.2] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-5.7.2.jar:5.7.2] at com.tito.userservice.filter.CustomAuthenticationFilter.attemptAuthentication(CustomAuthenticationFilter.java:43) ~[classes/:na] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:211) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:183) ~[spring-security-web-5.7.2.jar:5.7.2] at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.22.jar:5.3.22] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.22.jar:5.3.22] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1789) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
Which is the correct way to set up the PasswordEncoder in the new component-configuration?
Edit: adding login logic
AppUserService interface
package com.tito.userservice.services; import com.tito.userservice.domain.AppUser; import com.tito.userservice.domain.Role; import java.util.List; public interface AppUserService { AppUser saveUser(AppUser user); Role saveRole(Role role); void addRoleToUser(String username, String roleName); AppUser getUser(String username); List<AppUser>getUsers(); }
and the AppUserServiceImplementation:
package com.tito.userservice.services; import com.tito.userservice.domain.AppUser; import com.tito.userservice.domain.Role; import com.tito.userservice.repo.AppUserRepo; import com.tito.userservice.repo.RoleRepo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.ArrayList; import java.util.Collection; import java.util.List; @Service @RequiredArgsConstructor @Transactional @Slf4j public class AppUserServiceImpl implements AppUserService, UserDetailsService { private final AppUserRepo appUserRepo; private final RoleRepo roleRepo; private final PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { AppUser user =appUserRepo.findByUsername(username); if (user == null){ log.error("User not found in the database"); throw new UsernameNotFoundException("User not found in the databse"); }else { log.info("User found in the database: {}",username); } Collection<SimpleGrantedAuthority> authorities = new ArrayList<>(); user.getRoles().forEach(role -> { authorities.add(new SimpleGrantedAuthority(role.getName())); }); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } @Override public AppUser saveUser(AppUser user) { log.info("Saving new user {} to the database",user.getName()); user.setPassword(passwordEncoder.encode(user.getPassword())); return appUserRepo.save(user); } @Override public Role saveRole(Role role) { log.info("Saving new role {} to the database",role.getName()); return roleRepo.save(role); } @Override public void addRoleToUser(String username, String roleName) { log.info("Adding role {} to user {}",roleName,username ); //real life could be good, or must be good, to do validation of below AppUser user = appUserRepo.findByUsername(username); Role role = roleRepo.findByName(roleName); user.getRoles().add(role); } @Override public AppUser getUser(String username) { log.info("Fetching user {}",username ); return appUserRepo.findByUsername(username); } @Override public List<AppUser> getUsers() { log.info("Fetching all users"); return appUserRepo.findAll(); } }
Edit2: added CustomAuthenticationFilter.java code
CustomAuthenticationFilter.java
package com.tito.userservice.filter; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.tito.userservice.domain.AppUser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Component; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; import java.util.stream.Collectors; @Slf4j public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final AuthenticationManager authenticationManager; public CustomAuthenticationFilter(AuthenticationManager authenticationManager){ this.authenticationManager = authenticationManager; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = request.getParameter("username"); String password = request.getParameter("password"); log.info("Username is: {}", username); log.info("Password is: {}",password); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password); return authenticationManager.authenticate(authenticationToken); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { User user = (User)authentication.getPrincipal(); Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); //not for use in production, need to pass as encrypted and then decrypt here String access_token = JWT.create() .withSubject(user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() +10*60*1000)) .withIssuer(request.getRequestURL().toString()) .withClaim("roles",user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())) .sign(algorithm); String refresh_token = JWT.create() .withSubject(user.getUsername()) .withExpiresAt(new Date(System.currentTimeMillis() +30*60*1000)) .withIssuer(request.getRequestURL().toString()) .sign(algorithm); response.setHeader("access_token",access_token); response.setHeader("refresh_token",refresh_token); } }
Advertisement
Answer
Judging by the stacktrace I assume your CustomAuthenticationFilter
calls AuthenticationManager.authenticate()
method on a UsernamePasswordAuthenticationToken
object. That, by default, calls DaoAuthenticationProvider
, which is created with a DelegatingPasswordEncoder
, containing 11 password encoders, and which is trying to match passwords with this encoder (not your BCryptPasswordEncoder
bean).
This DelegatingPasswordEncoder
tries to match stored password, retrieved from your UserDetailsService
, and it assumes your password is stored with a key (prefix), like this:
{bcrypt}encodedPassword
You’ve registered a bean of type BCryptPasswordEncoder
and you’re encoding your password with it, so passwords are stored without {bcrypt} prefix (just the encoded password is stored), and when calling AuthenticationManager.authenticate()
mentioned DelegatingPasswordEncoder
sees null prefix and cannot find the corresponding PasswordEncoder
in a map – that’s what the error message is about.
So you can define a PasswordEncoder
bean as a DelegatingPasswordEncoder
, which, by default, uses BCryptPasswordEncoder
to encode passwords.
Like this:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfiguration { private final AuthenticationManagerBuilder authManagerBuilder; @Bean public PasswordEncoder encoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }
That way stored passwords will have {prefix} and DaoAuthenticationProvider
will be able to match passwords.
Also I advise to remove the BCryptPasswordEncoder
field from your SecurityConfiguration
class – you don’t need it there.
Also you can stuck with bean cycle dependencies, because your UserDetailsService
injects PasswordEncoder
, and PasswordEncoder
is defined in a configuration, where UserDetailsService
is injected, so you might also need refactoring.
For example, try to extract UserDetailsService
:
@Service @RequiredArgsConstructor @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { private final AppUserRepo appUserRepo; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // your logic here return new User(username, password, authorities); } }
Or define a bean of this type UserDetailsService
right in your SecurityConfiguration
class:
@Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfiguration { private final AppUserRepo appUserRepo; // other fields @Bean public UserDetailsService userDetailsService() { return username -> { // your logic here return new User(username, password, authorities); }; } }
Also you can:
- instead of a custom filter create your own login endpoint, where you can check password matches using
BCryptPasswordEncoder
directly; - create your own
AuthenticationProvider
with aBCryptPasswordEncoder
as a password encoder and use it in yourCustomAuthenticationFilter
to authenticate user.