Spring boot stores passwords hashed - which is good.
But - when moving to a new framework - how does that play out?
Spring Password Encoders
Spring boot requires some sort of PasswordEncoder.
If you look at PasswordEncoderFactories.createDelegatingPasswordEncoder() you can see that it can handle lots of different encoders - including out of date ones (so that you can still verify older hashes) - and that the default is bcrypt.
So - how does spring know what sort of hash you have?
This is stored in the database as part of the hash itself.
For example - with defaults - the values in the database look something like:
{bcrypt}$2a$10$....
The actual format of the bcrypt part is:
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]
So - here we have $2a with a cost of 10.
Checking hashes with password4j
In the ktor app - to check the hash you can use any library that can handle bcrypt - for this post - we'll look at password4j.
Password.check(plaintextPassword, storedHash).withBcrypt()
Now - it doesn't know about the prefix - so that has to be stripped off the hash before we check.
So - users will still be able to login after a migration with the same password.
Updating the hash
However - password4j recommends $2b and cost 12.
How can we update this hash when we don't know the user's password?
We can actually use password4j to provide us the new hash at check time.
Configuration
Add psw4j.properties to the classpath (src/main/resources)
global.banner=false
hash.bcrypt.minor=b
hash.bcrypt.rounds=12
(this also turns off the banner in the logs)
Simple user service implementation
This requires some sort of repository that allows you to get the stored hash for a username and to update the hash for a username.
{bcrypt}
prefix)andUpdate()
which gives us a new hash if necessary as well as telling us if it is validclass UserService(private val repository: UserRepository) {
fun checkPassword(username: String, password: String): Boolean {
var validPassword = false
repository.hashForUser(username)?.let { dbPassword ->
val check = Password.check(password, dbPassword.clean()).andUpdate().withBcrypt()
if (check.isVerified && check.isUpdated) {
repository.storeHash(username, check.hash.result)
}
validPassword = check.isVerified
}
return validPassword
}
companion object {
const val oldPrefix = "{bcrypt}"
}
private fun String.clean() = this.replace(oldPrefix, "")
}
Summary
This will allow the user to login with their existing password.
It will also update the user to password4j's recommeded $2b cost 12 setting - and update the database if required.
This update will be invisible to the user too.