Automatically converting password hashes in Grails spring-security-core
I was looking at this Stack Overflow question about converting password hashes and realized that it’s possible and rather convenient when using the spring-security-core plugin to automate the process.
To start, we’ll need a PasswordEncoder
that can work with both algorithms. Here I’m assuming that you’ll be converting from SHA-256 (optionally with a salt) to bcrypt, but the general approach is mostly independent of the algorithms. Sha256ToBCryptPasswordEncoder
will always hash new passwords using bcrypt, but can detect the difference between hashes from SHA-256 and bcrypt in isPasswordValid
:
package com.burtbeckwith.grails.security; import grails.plugin.springsecurity.authentication.encoding.BCryptPasswordEncoder; import org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder; public class Sha256ToBCryptPasswordEncoder implements org.springframework.security.authentication.encoding.PasswordEncoder { protected MessageDigestPasswordEncoder sha256PasswordEncoder; protected BCryptPasswordEncoder bcryptPasswordEncoder; public String encodePassword(String rawPass, Object salt) { return bcryptPasswordEncoder.encodePassword(rawPass, null); } public boolean isPasswordValid(String encPass, String rawPass, Object salt) { if (encPass.startsWith("$2a$10$") && encPass.length() == 60) { // already bcrypt return bcryptPasswordEncoder.isPasswordValid( encPass, rawPass, null); } if (encPass.length() == 64) { return sha256PasswordEncoder.isPasswordValid( encPass, rawPass, salt); } // TODO return false; } /** * Dependency injection for the bcrypt password encoder * @param encoder the encoder */ public void setBcryptPasswordEncoder(BCryptPasswordEncoder encoder) { bcryptPasswordEncoder = encoder; } /** * Dependency injection for the SHA-256 password encoder * @param encoder the encoder */ public void setSha256PasswordEncoder( MessageDigestPasswordEncoder encoder) { sha256PasswordEncoder = encoder; } }
This needs dependency injections for properly configured SHA-256 and bcrypt encoders, and we’ll see that in a bit.
Sha256ToBCryptPasswordEncoder
cannot make any changes as only password information is available, so we’ll subclass DaoAuthenticationProvider
and do this work in additionalAuthenticationChecks
:
package com.burtbeckwith.grails.security import grails.plugin.springsecurity.SpringSecurityUtils import grails.plugin.springsecurity.userdetails.GrailsUser import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.authentication.dao.DaoAuthenticationProvider import org.springframework.security.core.AuthenticationException import org.springframework.security.core.userdetails.UserDetails class PasswordFixingDaoAuthenticationProvider extends DaoAuthenticationProvider { def grailsApplication protected void additionalAuthenticationChecks( UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { super.additionalAuthenticationChecks userDetails, authentication // if we got this far the password was ok String oldHashedPassword = userDetails.getPassword() if (oldHashedPassword.startsWith('$2a$10$') && oldHashedPassword.length() == 60) { // already bcrypt return } if (oldHashedPassword.length() != 64) { // TODO return } String bcryptPassword = getPasswordEncoder().encodePassword( authentication.credentials, null) // use HQL to update the password in the database directly def conf = SpringSecurityUtils.securityConfig String userClassName = conf.userLookup.userDomainClassName Class<?> User = grailsApplication.getDomainClass(userClassName).clazz def args = [p: bcryptPassword] String hql = 'update ' + User.name + ' u set u.password=:p where ' if (userDetails instanceof GrailsUser) { hql += 'u.id=:id' args.id = userDetails.id } else { hql += 'u.' + conf.userLookup.usernamePropertyName + '=:un' args.un = userDetails.username } User.withNewSession { User.withTransaction { User.executeUpdate hql, args } } } }
Calling super.additionalAuthenticationChecks()
will ensure that a password was provided and it will be verified using either SHA-256 or bcrypt by Sha256ToBCryptPasswordEncoder
, so if there is no exception thrown it’s safe to update the password. Note that the update code is generic and can be made more compact by hard-coding your class and property names.
We register Sha256ToBCryptPasswordEncoder
as the passwordEncoder
bean, and create bcryptPasswordEncoder
and sha256PasswordEncoder
beans, configured with the SHA-256 settings that were being used, and the bcrypt settings that will be used (configure those in Config.groovy
as described in the docs). Also configure the bean override of daoAuthenticationProvider
to be a PasswordFixingDaoAuthenticationProvider
with the same configuration as is done in SpringSecurityCoreGrailsPlugin.groovy
with the addition of the grailsApplication
reference:
import grails.plugin.springsecurity.SpringSecurityUtils import grails.plugin.springsecurity.authentication.encoding.BCryptPasswordEncoder import org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder import com.burtbeckwith.grails.security.PasswordFixingDaoAuthenticationProvider import com.burtbeckwith.grails.security.Sha256ToBCryptPasswordEncoder beans = { def conf = SpringSecurityUtils.securityConfig bcryptPasswordEncoder(BCryptPasswordEncoder, conf.password.bcrypt.logrounds) // 10 sha256PasswordEncoder(MessageDigestPasswordEncoder, conf.password.algorithm) { encodeHashAsBase64 = conf.password.encodeHashAsBase64 // false iterations = conf.password.hash.iterations // 10000 } passwordEncoder(Sha256ToBCryptPasswordEncoder) { bcryptPasswordEncoder = ref('bcryptPasswordEncoder') sha256PasswordEncoder = ref('sha256PasswordEncoder') } daoAuthenticationProvider(PasswordFixingDaoAuthenticationProvider) { userDetailsService = ref('userDetailsService') passwordEncoder = ref('passwordEncoder') userCache = ref('userCache') saltSource = ref('saltSource') preAuthenticationChecks = ref('preAuthenticationChecks') postAuthenticationChecks = ref('postAuthenticationChecks') authoritiesMapper = ref('authoritiesMapper') hideUserNotFoundExceptions = conf.dao.hideUserNotFoundExceptions // true grailsApplication = ref('grailsApplication') } }
With this configuration, new users’ passwords will be hashed with bcrypt, and valid existing users’ passwords will be converted to bcrypt using the plaintext passwords used during login. Once your users are converted, back out these changes and convert to the standard bcrypt approach. This would involve deleting the grails.plugin.springsecurity.password.algorithm
attribute and all salt configuration since bcrypt doesn’t support a salt, deleting Sha256ToBCryptPasswordEncoder
and PasswordFixingDaoAuthenticationProvider
, and removing the bcryptPasswordEncoder
and sha256PasswordEncoder
bean definitions and passwordEncoder
and daoAuthenticationProvider
overrides from resources.groovy
since the beans configured by the plugin using Config.groovy
settings will be sufficient. Also if you had added salt to the User class encodePassword
method, e.g.
protected void encodePassword() { password = springSecurityService.encodePassword(password, username) }
convert it back to the default without a salt:
protected void encodePassword() { password = springSecurityService.encodePassword(password) }