Search code examples
javaspringspring-bootencryptionspring-security

Is it even possible to implement a database backed login process with encryption, using only Spring Security and Data JPA?


As the title states, I'm trying to implement something that, at this point, I'm not even sure is possible the way I imagined it. I want a very simple, database backed registration and login process in order to show different page contents to different users. So it should work like this:

  • User registers on a regsitration page, all checks are performed and User is created

  • Password is encrypted with Bcrypt and Username and Password are
    stored on a database using Spring Data JPA

  • User logs in over the "standard" Spring Security login form

  • My custom implementation of UserDetailsService fetches the database entry for the username

  • Spring security compares the passwords and logs in the user

  • After successful login, I get a principal and I'm able to display
    content based on that

The problem is in the encryption. Basically, I can't find a way to implement it without getting errors. No matter what I tried, it seems like the app does not even try to match the password from the login page with my encrypted password from the database. According to Baeldung, it should be enough to just define an encoder, and add it to the AuthenticationProvider. However, when I follow this tutorial, I get an error that says

Encoded password does not look like bcrypt

Here is my UserDetailsService, as stated in the comment, the password is, in fact, a valid, 60 character Bcrypt.

package com.example.demo;

import...

import java.util.Optional;

@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

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

        Optional<UserDB> userDB = userRepository.findById(s);

        User user = null;

        if (userDB.isPresent()) {
            System.out.println("present!");
            UserDB userDB2 = userDB.get();
            System.out.println(userDB2.getPassword());
            // The line above prints a valid BCrypt password with 60 characters
            user = new User(userDB2.getUsername(), userDB2.getPassword());
        }


        return user;
    }
}

I created a post here to ask for help, as the Autowiring of my UserDetailsService didn't work at first. There, the only answer I got was to define the Encoder in a different way to the tutorial:

@Bean(name = "passwordEncoder")
    @Qualifier("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

This changes the password that is saved to the repository from

$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq

to

{bcrypt}$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq

but then I get a new kind of error

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

The user who gave me the new definition of the Encoder linked me to this question, where someone had the same problem. This is what lead me to phrase this question the way I did.

The accepted answer there, as well as a few others, seem to either use Oauth2 (one answer uses the class ClientDetailsServiceConfigurer that I can't even import), disable encryption (or rather use an Encoder that does nothing) or use InMemoryAuthentication instead of a database. None of this is useful to me, as I want to use it for real and store my users permanently.

Is there some kind of problem in the current version of Spring Security that prevents this from working? It seems to me that the only step that's missing is the comparison of the encrypted password and the login input. It worked fine before with inMemoryAuth and no encryption.

Or do I have to use some additional service like OAuth2 ( I thought it was an addition to Spring Security, not a requirement)? I just want to know what works before I spend another three days trying out different tutorials that make it seem like it should work really easy, and then it turns out it somehow doesn't work at all.

I'll show you all the other relevant parts of my code, maybe I just made some super simple mistake there:

First the WebSecurityConfig

package com.example.demo;

import...

@Configuration
@ComponentScan(basePackages = { "com.example.demo" })
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/main", "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    @Autowired
    private MyUserDetailsService userDetailsService;

@Bean(name = "passwordEncoder")
    @Qualifier("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
}

The Controller for the registration:

@PostMapping("/register")
    public String workRegistrationForm(Model model, @ModelAttribute("user") UserDto newUser) {
        System.out.println(newUser.getUsername() + ", " + newUser.getEmail());
        String encodedPW = encoder.encode(newUser.getPassword());
        UserDB newUserDB = new UserDB(newUser.getUsername(), encodedPW);
        userRepository.save(newUserDB);
        return "redirect:/main";
    }

MVC Config that adds the login view:

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/securedtwo").setViewName("securedtwo");
        registry.addViewController("/login").setViewName("login");
    }

}

The login.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
    Invalid username and password.
</div>
<div th:if="${param.logout}">
    You have been logged out.
</div>
<form th:action="@{/login}" method="post">
    <div><label> User Name : <input type="text" name="username"/> </label></div>
    <div><label> Password: <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>

The User that implements UserDetails (UserDB is basically the same without the authorities):

public class User implements UserDetails {

    String password;
    String username;

    public User(String password, String username) {
        this.password = password;
        this.username = username;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        ArrayList<GrantedAuthority> rights = new ArrayList<>();
        rights.add(new SimpleGrantedAuthority("USER"));

        return rights;
    }

..Getters and Setters

Please help me. Anything you can tell me is appreciated. It seemed like this should be super simple, but so far it's been nothing but frustrating for me. I really can't tell what it is I might be doing wrong, and the worst is it feels like I'm really close.

Another option I thought of: In case this can't be made to work, could I simply do the check myself? What class and method would I have to override to be able to do the simple task of comparing the passwords with my own logic and just tell Spring Security "This user is valid"?

Thanks in advance!


Solution

  • It looks like you have bcrypt passwords in your database that are not labeled as bcrypt. Try:

    public PasswordEncoder passwordEncoder() {                                      
        DelegatingPasswordEncoder encoder =                                         
            PasswordEncoderFactories.createDelegatingPasswordEncoder();             
                                                                                    
        encoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());   
                                                                                    
        return encoder;                                                             
    }
    

    This should interpret any password without an "{id}" as bcrypt.

    [Updated.]

    One other change driven from. my code is to add a method to your WebSecurityConfigurerAdapter implementation:

    public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
        ...
    
        @Autowired private UserDetailsService userDetailsService;
        @Autowired private PasswordEncoder passwordEncoder;
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
        }
    
        ...
    }
    

    Hopefully, this helps.