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!
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.