(Quick Reference)
11 Password and Account Protection - Reference Documentation
Authors: Burt Beckwith, Beverley Talbott
Version: 2.0.0
11 Password and Account Protection
The sections that follow discuss approaches to protecting passwords and user accounts.
11.1 Password Hashing
By default the plugin uses the bcrypt algorithm to hash passwords. You can customize this with the
grails.plugin.springsecurity.password.algorithm
attribute as described below. In addition you can increase the security of your passwords by adding a salt, which can be a field of the
UserDetails
instance, a global static value, or any custom value you want.
bcrypt is a much more secure alternative to the message digest approaches since it supports a customizable work level which when increased takes more computation time to hash the users' passwords, but also dramatically increases the cost of brute force attacks. Given how easy it is to
use GPUs to crack passwords, you should definitely consider using bcrypt for new projects and switching to it for existing projects. Note that due to the approach used by bcrypt, you cannot add an additional salt like you can with the message digest algorithms.
Enable bcrypt by using the
'bcrypt'
value for the
algorithm
config attribute:
grails.plugin.springsecurity.password.algorithm = 'bcrypt'
and optionally changing the number of rekeying rounds (which will affect the time it takes to hash passwords), e.g.
grails.plugin.springsecurity.password.bcrypt.logrounds = 15
Note that the number of rounds must be between 4 and 31.
PBKDF2 is also supported.
The table shows configurable password hashing attributes.
If you want to use a message digest hashing algorithm, see
this Java page for the available algorithms.
Property | Default | Description |
---|
password.algorithm | 'bcrypt' | passwordEncoder algorithm; 'bcrypt' to use bcrypt, 'pbkdf2' to use PBKDF2, or any message digest algorithm that is supported in your JDK |
password.encodeHashAsBase64 | false | If true , Base64-encode the hashed password. |
password.bcrypt.logrounds | 10 | the number of rekeying rounds to use when using bcrypt |
password.hash.iterations | 10000 | the number of iterations which will be executed on the hashed password/salt. |
11.2 Salted Passwords
The Spring Security plugin uses hashed passwords and a digest algorithm that you specify. For enhanced protection against dictionary attacks, you should use a salt in addition to digest hashing.
Note that if you use bcrypt (the default setting) or pbkdf2, do not configure a salt (e.g. the dao.reflectionSaltSourceProperty
property or a custom saltSource
bean) because these algorithms use their own internally.
There are two approaches to using salted passwords in the plugin - defining a field in the
UserDetails
class to access by reflection, or by directly implementing
SaltSource yourself.
dao.reflectionSaltSourceProperty
Set the
dao.reflectionSaltSourceProperty
configuration property:
grails.plugin.springsecurity.dao.reflectionSaltSourceProperty = 'username'
This property belongs to the
UserDetails
class. By default it is an instance of
grails.plugin.springsecurity.userdetails.GrailsUser
, which extends the standard Spring Security
User class and not your 'person' domain class. This limits the available fields unless you use a
custom UserDetailsService
.
As long as the username does not change, this approach works well for the salt. If you choose a property that the user can change, the user cannot log in again after changing it unless you re-hash the password with the new value. So it's best to use a property that doesn't change.
Another option is to generate a random salt when creating users and store this in the database by adding a new field to the 'person' class. This approach requires a custom
UserDetailsService
because you need a custom
UserDetails
implementation that also has a 'salt' property, but this is more flexible and works in cases where users can change their username.
SystemWideSaltSource and Custom SaltSource
Spring Security supplies a simple
SaltSource
implementation,
SystemWideSaltSource, which uses the same salt for each user. It's less robust than using a different value for each user but still better than no salt at all.
An example override of the salt source bean using SystemWideSaltSource would look like this:
import org.springframework.security.authentication.dao.SystemWideSaltSource
beans = {
saltSource(SystemWideSaltSource) {
systemWideSalt = 'the_salt_value'
}
}
To have full control over the process, you can implement the
SaltSource
interface and replace the plugin's implementation with your own by defining a bean in
grails-app/conf/spring/resources.groovy
with the name
saltSource
:
beans = {
saltSource(com.foo.bar.MySaltSource) {
// set properties
}
}
Hashing Passwords
Regardless of the implementation, you need to be aware of what value to use for a salt when creating or updating users, for example, in a
UserController
's
save
or
update
action. When hashing the password, you use the two-parameter version of
springSecurityService.encodePassword()
:
class UserController { def springSecurityService def save() {
def userInstance = new User(params)
userInstance.password = springSecurityService.encodePassword(
params.password, userInstance.username)
if (!userInstance.save(flush: true)) {
render view: 'create', model: [userInstance: userInstance]
return
} flash.message = "The user was created"
redirect action: show, id: userInstance.id
} def update() {
def userInstance = User.get(params.id) if (params.password) {
params.password = springSecurityService.encodePassword(
params.password, userInstance.username)
}
userInstance.properties = params
if (!userInstance.save(flush: true)) {
render view: 'edit', model: [userInstance: userInstance]
return
} if (springSecurityService.loggedIn &&
springSecurityService.principal.username ==
userInstance.username) {
springSecurityService.reauthenticate userInstance.username
} flash.message = "The user was updated"
redirect action: show, id: userInstance.id
}
}
If you are encoding the password in the User domain class (using beforeInsert
and encodePassword
) then don't call springSecurityService.encodePassword()
in your controller since you'll double-hash the password and users won't be able to log in. It's best to encapsulate the password handling logic in the domain class. In newer versions of the plugin (version 1.2 and higher) code is auto-generated in the user class so you'll need to adjust that password hashing for your salt approach.
11.3 Account Locking and Forcing Password Change
Spring Security supports four ways of disabling a user account. When you attempt to log in, the
UserDetailsService
implementation creates an instance of
UserDetails
that uses these accessor methods:
isAccountNonExpired()
isAccountNonLocked()
isCredentialsNonExpired()
isEnabled()
If you use the
s2-quickstart script to create a user domain class, it creates a class with corresponding properties to manage this state.
When an accessor returns
true
for
accountExpired
,
accountLocked
, or
passwordExpired
or returns
false
for
enabled
, a corresponding exception is thrown:
You can configure an exception mapping in
Config.groovy
to associate a URL to any or all of these exceptions to determine where to redirect after a failure, for example:
grails.plugin.springsecurity.failureHandler.exceptionMappings = [
'org.springframework.security.authentication.LockedException':
'/user/accountLocked',
'org.springframework.security.authentication.DisabledException':
'/user/accountDisabled',
'org.springframework.security.authentication.AccountExpiredException':
'/user/accountExpired',
'org.springframework.security.authentication.CredentialsExpiredException':
'/user/passwordExpired'
]
Without a mapping for a particular exception, the user is redirected to the standard login fail page (by default
/login/authfail
), which displays an error message from this table:
Property | Default |
---|
errors.login.disabled | "Sorry, your account is disabled." |
errors.login.expired | "Sorry, your account has expired." |
errors.login.passwordExpired | "Sorry, your password has expired." |
errors.login.locked | "Sorry, your account is locked." |
errors.login.fail | "Sorry, we were not able to find a user with that username and password." |
You can customize these messages by setting the corresponding property in
Config.groovy
, for example:
grails.plugin.springsecurity.errors.login.locked = "None shall pass."
You can use this functionality to manually lock a user's account or expire the password, but you can automate the process. For example, use the
Quartz plugin to periodically expire everyone's password and force them to go to a page where they update it. Keep track of the date when users change their passwords and use a Quartz job to expire their passwords once the password is older than a fixed max age.
Here's an example for a password expired workflow. You'd need a simple action to display a password reset form (similar to the login form):
def passwordExpired() {
[username: session['SPRING_SECURITY_LAST_USERNAME']]
}
and the form would look something like this:
<div id='login'>
<div class='inner'>
<g:if test='${flash.message}'>
<div class='login_message'>${flash.message}</div>
</g:if>
<div class='fheader'>Please update your password..</div>
<g:form action='updatePassword' id='passwordResetForm'
class='cssform' autocomplete='off'>
<p>
<label for='username'>Username</label>
<span class='text_'>${username}</span>
</p>
<p>
<label for='password'>Current Password</label>
<g:passwordField name='password' class='text_' />
</p>
<p>
<label for='password'>New Password</label>
<g:passwordField name='password_new' class='text_' />
</p>
<p>
<label for='password'>New Password (again)</label>
<g:passwordField name='password_new_2' class='text_' />
</p>
<p>
<input type='submit' value='Reset' />
</p>
</g:form>
</div>
</div>
It's important that you not allow the user to specify the username (it's available in the HTTP session) but that you require the current password, otherwise it would be simple to forge a password reset.
The GSP form would submit to an action like this one:
def updatePassword() {
String username = session['SPRING_SECURITY_LAST_USERNAME']
if (!username) {
flash.message = 'Sorry, an error has occurred'
redirect controller: 'login', action: 'auth'
return
} String password = params.password
String newPassword = params.password_new
String newPassword2 = params.password_new_2
if (!password || !newPassword || !newPassword2 ||
newPassword != newPassword2) {
flash.message =
'Please enter your current password and a valid new password'
render view: 'passwordExpired',
model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
} User user = User.findByUsername(username)
if (!passwordEncoder.isPasswordValid(user.password,
password, null /*salt*/)) {
flash.message = 'Current password is incorrect'
render view: 'passwordExpired',
model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
} if (passwordEncoder.isPasswordValid(user.password, newPassword,
null /*salt*/)) {
flash.message =
'Please choose a different password from your current one'
render view: 'passwordExpired',
model: [username: session['SPRING_SECURITY_LAST_USERNAME']]
return
} user.password = newPassword
user.passwordExpired = false
user.save() // if you have password constraints check them here redirect controller: 'login', action: 'auth'
}
User Cache
If the
cacheUsers
configuration property is set to
true
, Spring Security caches
UserDetails
instances to save trips to the database. (The default is
false
.) This optimization is minor, because typically only two small queries occur during login -- one to load the user, and one to load the authorities.
If you enable this feature, you must remove any cached instances after making a change that affects login. If you do not remove cached instances, even though a user's account is locked or disabled, logins succeed because the database is bypassed. By removing the cached data, you force at trip to the database to retrieve the latest updates.
Here is a sample Quartz job that demonstrates how to find and disable users with passwords that are too old:
package com.mycompany.myappclass ExpirePasswordsJob { static triggers = {
cron name: 'myTrigger', cronExpression: '0 0 0 * * ?' // midnight daily
} def userCache void execute() { def users = User.executeQuery(
'from User u where u.passwordChangeDate <= :cutoffDate',
[cutoffDate: new Date() - 180]) for (user in users) {
// flush each separately so one failure
// doesn't rollback all of the others
try {
user.passwordExpired = true
user.save(flush: true)
userCache.removeUserFromCache user.username
}
catch (e) {
log.error "problem expiring password for user $user.username : $e.message", e
}
}
}
}