Search code examples
javaspring-bootspring-securityspring-cache

Caching UserDetails using Springboot


I am trying to implement a UserCache in my application to avoid to make multiple calls to the User table in the case I am using the basic authentication. I created my CacheConfig following the accepted answer of this topic, in which the CachingUserDetailsService is used to manage the user cache. Bellow is the code of the UserService, CacheConfig and SecurityConfig:


public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    public UserService(UserRepository repository) {
        this.userRepository = repository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        AddInUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("O usuário " + username + " não pode ser encontrado!"));

        UserDetails userDetails = User
                .builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .roles("USER")
                .build();


        return userDetails;
    }

    @Transactional
    public AddInUser save(AddInUser user) {
        return userRepository.save(user);
    }


}

@EnableCaching
@Configuration
public class CacheConfig {

    public static final String USER_CACHE = "userCache";

    /**
     * Define cache strategy
     *
     * @return CacheManager
     */
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
        List<Cache> caches = new ArrayList<>();

        //Failure after 5 minutes of caching
        caches.add(new GuavaCache(CacheConfig.USER_CACHE,
                CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build()));

        simpleCacheManager.setCaches(caches);

        return simpleCacheManager;
    }

    @Bean
    public UserCache userCache() throws Exception {
        Cache cache = cacheManager().getCache("userCache");
        return new SpringCacheBasedUserCache(cache);
    }


}
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    };

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserCache userCache;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
                .and()
                .authorizeRequests()
                    .antMatchers("/api/**")
                        .authenticated()
                .and()
                    .httpBasic();
    }

    @Override
    @Bean
    public UserDetailsService userDetailsService() {
        UserService userService = new UserService(userRepository);
        CachingUserDetailsService cachingUserService = new CachingUserDetailsService(userService);
        cachingUserService.setUserCache(this.userCache);
        return cachingUserService;
    }

}

The first call works well because it makes the call to the UserRepository. But on the second, it does not make the call to the repository (as expected) but I am getting the following WARN from BCryptPasswordEncoder:

2020-09-24 08:43:51.327  WARN 24624 --- [nio-8081-exec-4] o.s.s.c.bcrypt.BCryptPasswordEncoder     : Empty encoded password

The warning is clear in its meaning and it fails to authenticate de user because of the null password. But I cannot understand why the user retried from cache has a null password if it was correctly stored. I am not sure how to solve it using the cache. Any thoughts?

Thank you very much for your help!


Solution

  • @M.Deinum comment is absolutely correct. You can refer to the doc here.

    Note that this implementation is not immutable. It implements the CredentialsContainer interface, in order to allow the password to be erased after authentication. This may cause side-effects if you are storing instances in-memory and reusing them. If so, make sure you return a copy from your UserDetailsService each time it is invoked.

    You can check the Spring-security source code if you are curious more:

    private boolean eraseCredentialsAfterAuthentication = true;
        
    ...
    
    if (result != null) {
        if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
            // Authentication is complete. Remove credentials and other secret data
            // from authentication
            ((CredentialsContainer) result).eraseCredentials();
        }
        // If the parent AuthenticationManager was attempted and successful then it
        // will publish an AuthenticationSuccessEvent
        // This check prevents a duplicate AuthenticationSuccessEvent if the parent
        // AuthenticationManager already published it
        if (parentResult == null) {
            this.eventPublisher.publishAuthenticationSuccess(result);
        }
    
        return result;
    }
    

    And User.java source code:

    @Override
    public void eraseCredentials() {
        this.password = null;
    }
    

    By the way, it looks weird to cache login user that way. During login, it is better to get fresh record from DB instead of from cache. You can use cached user at other place but seldom see it is used during login.

    If you really need to do that, you can change the default flag to false as mentioned in doc, just inject AuthenticationManager and call:

    setEraseCredentialsAfterAuthentication(false)