I'm developing a login service with kotlin and Spring Boot 2.5.5, and with a MySQL database. Everything seems to be working correctly (register user, delete user, update user), except that when I try to call the login endpoint from postman, I get an error:
2021-10-26 18:16:28.558 WARN 26076 --- [nio-8080-exec-2] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
I've searched all over stackoverflow and I saw that other people have this problem, but none of the suggested answers worked for me (or even applied to my situation in some cases)
My SecurityConfig class:
@Configuration
@EnableWebSecurity
class SecurityConfig(private val dataSource: DataSource) : WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser("user").password("password")
.authorities("USER", "ADMIN", "CONTRIBUTOR")
}
override fun configure(http: HttpSecurity?) {
http!!.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasAuthority("ADMIN")
.antMatchers("/contributor/**").hasAuthority("CONTRIBUTOR")
.antMatchers("/anonymous*").anonymous()
.antMatchers("/login/**").permitAll()
.antMatchers("/login*").permitAll()
.antMatchers("/**").hasAuthority("USER")
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout().permitAll()
}
@Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth.jdbcAuthentication()
.passwordEncoder(passwordEncoder())
.dataSource(dataSource)
.usersByUsernameQuery("select username,encrypted_password,\'true\' from dragonline.user where username=?")
.authoritiesByUsernameQuery("select user_username,roles_name from dragonline.user_has_roles where user_username=?")
}
}
class AppInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
val root = AnnotationConfigWebApplicationContext()
root.register(SecurityConfig::class.java)
servletContext.addListener(ContextLoaderListener(root))
servletContext.addFilter("securityFilter", DelegatingFilterProxy("springSecurityFilterChain"))
.addMappingForUrlPatterns(null, false, "/*")
}
}
My login controller:
@RestController
@RequestMapping(path = ["/login"])
class Login(private val userRegistrationService: UserRegistrationService) {
@PostMapping(path = ["/register"], consumes = [MediaType.APPLICATION_JSON_VALUE])
fun register(@RequestBody user: UserRegistrationDTO, response: HttpServletResponse): UserRegistrationDTO {
user.confirmPassword()
val domainUser = user.toDomain()
userRegistrationService.saveUser(domainUser)
response.status = HttpServletResponse.SC_CREATED
return user.maskPassword()
}
@CrossOrigin
@PostMapping(path = ["/find-user"], consumes = [MediaType.APPLICATION_JSON_VALUE])
fun login(@RequestBody user: UserLoginDTO, response: HttpServletResponse): UserLoginDTO {
val userFound = userRegistrationService.findUser(user.username)
println("The password stored ind DB is ${userFound?.encryptedPassword}")
if (passwordEncoder().matches(userFound?.encryptedPassword, user.password)) {
response.status = HttpServletResponse.SC_OK
response.setHeader(
"Session-ID",
"${passwordEncoder().encode(user.username)}@.@${passwordEncoder().encode(user.password)}"
)
} else {
response.status = HttpServletResponse.SC_NOT_FOUND
}
return user.maskPassword()
}
@GetMapping(path = ["/delete-user/{userId}"])
fun deleteUser(@PathVariable userId: String) {
userRegistrationService.deleteUser(userId)
}
}
The entry in my DB looks like this:
'test', '$2a$10$Z/FSxELmMtV2zpgFVYzDi.dprUybnzJxF6f/kZan7DqHfQ9VDjmQq', '[email protected]', 'Tester', 'Testing a Test', NULL, '0'
The columns are username, encrypted_password, email, name, lastnames, session_id, session_active respectively
My UserService:
@Service
@Transactional
class UserRegistrationService(
private val userRegistrationRepository: UserRegistrationRepository
) {
fun findUser(userId: String) =
userRegistrationRepository.findByUsername(userId) ?: userRegistrationRepository.findByEmail(userId)
fun saveUser(user: UserRegistrationData) =
userRegistrationRepository.save(user.toPersistence())
fun deleteUser(userId: String) {
val user = userRegistrationRepository.findByUsername(userId) ?: userRegistrationRepository.findByEmail(userId)
?: throw Exception("The user can't be deleted, because it doesn't exist")
user.username.let {
userRegistrationRepository.deleteByUsername(it)
}
}
}
My Repository:
@Repository
interface UserRegistrationRepository: JpaRepository<User, String> {
fun findByUsername(username: String): User?
fun findByEmail(email: String): User?
fun deleteByUsername(username: String): Long
fun deleteByEmail(email: String): Long
}
My extension functions file (I put my Eencoder bean here):
fun UserRegistrationData.toPersistence() = User(
username = this.username,
encryptedPassword = this.encryptedPassword,
email = this.email,
name = this.name,
lastnames = this.lastnames,
roles = this.roles.map { it.toPersistence() }.toHashSet()
)
fun UserRole.toPersistence() = Role(
name = this.name
)
fun UserLoginDTO.maskPassword() = UserLoginDTO(
username = this.username,
password = "***********"
)
fun UserRegistrationDTO.confirmPassword() {
if (this.password != this.confirmPassword) throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"The passwords don't match"
)
}
fun UserRegistrationDTO.maskPassword() = UserRegistrationDTO(
username = this.username,
password = "***********",
confirmPassword = "***********",
email = this.email,
name = this.name,
lastnames = this.lastnames,
admin = this.admin,
contributor = this.contributor
)
fun UserRegistrationDTO.toDomain(): UserRegistrationData {
val user = UserRegistrationData(
name = this.name,
lastnames = this.lastnames,
email = this.email,
username = this.username,
encryptedPassword = passwordEncoder().encode(this.password),
roles = mutableListOf(UserRole.USER)
)
if(this.admin) user.roles.add(UserRole.ADMIN)
if(this.contributor) user.roles.add(UserRole.CONTRIBUTOR)
return user
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
My models (They're not all in the same package, that's why there's so many of them):
data class UserRegistrationDTO(
val name: String? = null,
val lastnames: String? = null,
@field:NotBlank
val username: String,
@field:Email
val email: String,
@field:NotBlank
val password: String,
@field:NotBlank
val confirmPassword: String,
val admin: Boolean = false,
val contributor: Boolean = false
)
data class UserLoginDTO(
val username: String,
val password: String
)
data class UserRegistrationData(
val name: String? = null,
val lastnames: String? = null,
@field:NotBlank
val username: String,
@field:Email
val email: String,
@field:NotBlank
val encryptedPassword: String,
val roles: MutableList<UserRole> = mutableListOf(UserRole.USER)
)
class User(
@Id
var username: String,
@Column
var email: String,
@Column
var name: String?,
@Column
var lastnames: String?,
@Column
var encryptedPassword: String,
@OneToMany
@JoinTable(
name = "user_has_roles",
joinColumns = [JoinColumn(name = "user_username")],
inverseJoinColumns = [JoinColumn(name = "roles_name")]
)
var roles: Set<Role>? = null
)
And finally my Postman Request
If there's any more information that you need, please let me know.
CodePudding user response:
The problem you are having is because you mixed up the inputs to the BCryptPasswordEncoder#matches
function.
your error message pointed me to this line in the source code, which gave me a hint of what was wrong. Reading the source code is a good way to find out what is wrong.
The api tells us that matches(CharSequence rawPassword, String encodedPassword)
and you gave it the encoded as the raw, and the raw as the encoded.