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) }
Exactly what I did in a customers Java Spring project 2 years ago to smoothly convert users from md5 to bcrypt. At a certain point in time, all remaining md5 passwords were reset and the users informed to force them to bcrypt’d passwords.
Nice to see you use the same approach.
And if upgrading from Spring Security plugin 1.2 to 2.0, the other little config things that were needed: in Config.groovy, set grails.plugin.springsecurity.password.algorithm=’SHA-256′ and
grails.plugin.springsecurity.password.hash.iterations = 1
Thanks Charles Wood.
Seting up grails.plugin.springsecurity.password.hash.iterations = 1 very helped me to understand why I couldn’t authorize after update from old spring-security to the last one.
Thanks a lot.
Of course, setting the hash iterations to 1 seems to defeat the purpose of using bcrypt in my mind; but, that’s the next step I guess.
That setting applies to regular hashed password – the grails.plugin.springsecurity.password.bcrypt.logrounds property is the one to use for bcrypt.
Neat!
The “10” in “$2a$10$” seems to be the cost paameter of bcrypt, as configured by grails.plugin[s].springsecurity.password.bcrypt.logrounds. So if somebody configures a different number of logrounds, maybe to adapt to faster hardware in the future, the hash type detection does not work anymore.
Can you change the code to just check for “$2a$”, or maybe just the three “$” in the beginning of the string?
According to Wikipedia, the prefix can also be “2y” instead of “$2a$”, but I haven’t seen that in Grails/spring-security.
http://en.wikipedia.org/wiki/Bcrypt
In 2.0 I’m using the newly added class from Spring Security, so you should mention this to them