Getting authentication with Spring Security (s2) set up on Grails is nice and easy; getting your s2 passwords salted with a unique value, not so much. There's a real nice grails s2 plugin that Burt Beckwith maintains. It actually is quite well documented, but there's an awful lot of places where the code is still a ways ahead of the documentation.
So here's a quick tutorial to get your grails passwords salted:
1 Install S2
First install the spring-security plugin:
$ grails install-plugin spring-security-core
Then create your "user" and "role" domain objects. You can call the "user" and "role" classes whatever you want, and put them in whatever package you choose. I called mine cq.User
and cq.Role
:
$ grails s2-quickstart cq User Role
2 Basic Configuration
The s2-quickstart
script will automatically add the following to your grails-app/conf/Config.groovy
config file:
// Added by the Spring Security Core plugin:
grails.plugins.springsecurity.userLookup.userDomainClassName = 'cq.User'
grails.plugins.springsecurity.userLookup.authorityJoinClassName = 'cq.UserRole'
grails.plugins.springsecurity.authority.className = 'cq.Role'
If you know that your usernames won't change, you can use them to salt the password. While not ideal as salts (an attacker can still build out rainbow tables of common username/password combinations pretty easily), they're a lot better than no salt at all.
To use the username as a salt, all you need to do is add one config setting to your Config.groovy
. Unfortunately, the s2 manual had the wrong name for this setting; this is the right setting to add to Config.groovy
:
grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username'
I'd also recommend turning on the setting that encodes the hashed passwords as base-64 strings (instead of strings of hex digits; it'll shave a dozen characters off the size of the hashed password):
grails.plugins.springsecurity.password.encodeHashAsBase64 = true
3 Basic Password Hashing
The other thing you need to do is make sure you hash the password with the salt whenever the password is saved. The s2 quickstart tutorial directs you to do this in your user controller. Don't; you should do this in the domain models, so you don't repeat yourself.
So update your "user" class to look like this:
package cq
class User {
def springSecurityService
String username
String password
boolean enabled
boolean accountExpired
boolean accountLocked
boolean passwordExpired
static mapping = {
// password is a keyword in some sql dialects, so quote with backticks
// password is stored as 44-char base64 hashed value
password column: '`password`', length: 44
}
static constraints = {
username blank: false, size: 1..50, unique: true
password blank: false, size: 8..100
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password'))
encodePassword()
}
Set getAuthorities() {
UserRole.findAllByUser(this).collect { it.role } as Set
}
protected encodePassword() {
password = springSecurityService.encodePassword(password, username)
}
}
The main difference between the above and what s2-quickstart
generates is the internal encodePassword()
method. When a new user is saved, the beforeInsert()
method will be called by the gorm framework, and our user class will hash the password, with the username as a salt. When an existing user is updated, the beforeUpdate()
method will be called; it will check if the password has changed, and if it has, it will also hash the new password the same way.
This way you never have to hash a user's password in a controller or other code; just pass it on through to the domain model like any other property.
4 A Quick Test
At this point you've done enough to store passwords hashed with the username as a salt. Test it out by adding some test users in your bootstrap code, and a check for authenticated users on your home page.
In grails-app/conf/BootStrap.groovy
, create and save a new test user:
class BootStrap {
def init = { servletContext ->
new cq.User(username: 'test', enabled: true, password: 'password').save(flush: true)
}
def destroy = {
}
}
And in grails-app/views/index.gsp
, add this to the top of the body:
...
<body>
<sec:ifLoggedIn><h1>Hey, I know you; you're <sec:username/>!</h1></sec:ifLoggedIn>
<sec:ifNotLoggedIn><h1>Who are you?</h1></sec:ifNotLoggedIn>
...
Now run your app (with clean
, just to make sure everything gets rebuilt properly):
$ grails clean && grails run-app
Navigate to http://localhost:8080/cq/login (where cq
is the name of your app), and login with a username of test and a password of password. Pretty sweet what you get (just about) out of the box, huh?
5 Adding a Unique Salt
Let's kick it up a notch. To create a unique salt for each user (making it impractical for an attacker to use rainbow tables to crack the hashed passwords), add a salt
field to your "user" class, and override the getter for this field to initialize it with a unique salt:
package cq
import java.security.SecureRandom; // add
class User {
def springSecurityService
String username
String password
String salt // add
boolean enabled
boolean accountExpired
boolean accountLocked
boolean passwordExpired
static mapping = {
// password is a keyword in some sql dialects, so quote with backticks
// password is stored as 44-char base64 hashed value
password column: '`password`', length: 44
}
static constraints = {
username blank: false, size: 1..50, unique: true
password blank: false, size: 8..100
// salt is stored as 64-char base64 value
salt maxSize: 64 // add
}
def beforeInsert() {
encodePassword()
}
def beforeUpdate() {
if (isDirty('password'))
encodePassword()
}
// add:
String getSalt() {
if (!this.salt) {
def rnd = new byte[48];
new SecureRandom().nextBytes(rnd)
this.salt = rnd.encodeBase64()
}
this.salt
}
Set getAuthorities() {
UserRole.findAllByUser(this).collect { it.role } as Set
}
protected encodePassword() {
password = springSecurityService.encodePassword(password, salt) // update
}
}
Don't forget to also update the encodePassword()
method to hash the password with the salt
field, instead of the username
field.
6 Adding Custom UserDetails
Here's where it gets tricky. S2 maintains a user class for authentication called UserDetails
that's completely separate from your "user" domain model. So you have to provide a custom UserDetailsService
class that creates a custom UserDetails
object given a username, as well as a custom SaltSource
helper-class to extract the salt value from the custom UserDetails
.
The good news is that you don't have to write a whole lot of code to do this. Create the following class as src/groovy/cq/MyUserDetailsService.groovy
(or with whatever namespace and classname you like):
package cq
import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class MyUserDetailsService extends GormUserDetailsService {
protected UserDetails createUserDetails(user, Collection authorities) {
new MyUserDetails((GrailsUser) super.createUserDetails(user, authorities),
user.salt
)
}
}
And create the following as src/groovy/cq/MyUserDetails.groovy
:
package cq
import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser
class MyUserDetails extends GrailsUser {
public final String salt
MyUserDetails(GrailsUser base, String salt) {
super(base.username, base.password, base.enabled,
base.accountNonExpired, base.credentialsNonExpired, base.accountNonLocked,
base.authorities, base.id)
this.salt = salt;
}
}
And create the following as src/groovy/cq/MySaltSource.groovy
:
package cq
import org.springframework.security.authentication.dao.ReflectionSaltSource
import org.springframework.security.core.userdetails.UserDetails
class MySaltSource extends ReflectionSaltSource {
Object getSalt(UserDetails user) {
user[userPropertyToUse]
}
}
Note that if you use a java UserDetails
implementation, instead of a groovy implementation, you can just use ReflectionSaltSource
directly — you need to customize it only to do groovy "reflection" (it does java reflection just fine).
7 Configuring UserDetails
Finally, you can configure s2 to use your custom UserDetails
class by adding the following to your grails-app/conf/spring/resources.groovy
:
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH
beans = {
userDetailsService(cq.MyUserDetailsService) {
sessionFactory = ref('sessionFactory')
transactionManager = ref('transactionManager')
}
saltSource(cq.MySaltSource) {
userPropertyToUse = CH.config.grails.plugins.springsecurity.dao.reflectionSaltSourceProperty
}
}
If you were to implement your UserDetails
class in java you could omit the saltSource
bean (since it comes configured out-of-the-box to do reflection on java classes). Otherwise, the one last piece of the puzzle is to go back and change the dao.reflectionSaltSourceProperty
setting in your grails-app/conf/Config.groovy
to your new salt
field:
grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'salt'
8 A Real Test
Now to verify that all this stuff is working (and will still work when you mess around with your user domain model in the future), you need some integration tests. First, let's tackle the password-hashing scheme; create a test/integration/cq/UsersTests.groovy
class, and dump this in it:
package cq
class UserTests extends GroovyTestCase {
def springSecurityService
void testPasswordIsEncodedWhenUserIsCreated() {
def user = new User(username: 'testuser1', password: 'password').save(flush: true)
assertEquals springSecurityService.encodePassword('password', user.salt), user.password
}
void testPasswordIsReEncodedWhenUserIsUpdatedWithNewPassword() {
def user = new User(username: 'testuser1', password: 'password').save(flush: true)
// update password
user.password = 'password1'
user.save(flush: true)
assertEquals springSecurityService.encodePassword('password1', user.salt), user.password
}
void testPasswordIsNotReEncodedWhenUserIsUpdatedWithoutNewPassword() {
def user = new User(username: 'testuser1', password: 'password').save(flush: true)
// update user, but not password
user.enabled = true
user.save(flush: true)
assertEquals springSecurityService.encodePassword('password', user.salt), user.password
}
void testPasswordIsNotReEncodedWhenUserIsReloaded() {
new User(username: 'testuser1', password: 'password').save(flush: true)
// reload user
def user = User.findByUsername('testuser1')
assertNotNull user
assertEquals springSecurityService.encodePassword('password', user.salt), user.password
}
}
Now the authentication part. This is a bit awkward, because what we're really testing is that your custom UserDetails
is implemented and configured correctly, but let's pretend that it's testing your login controller and stick it in your LoginControllerTests
anyway. Create a test/integration/cq/LoginControllerTests.groovy
class, and put this in it:
package cq
import java.security.Principal
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
class LoginControllerTests extends GroovyTestCase {
def daoAuthenticationProvider
void testAuthenticationFailsWithIncorrectPassword() {
def user = new User(
username: 'testuser1', password: 'password', enabled: true
).save(flush: true)
def token = new UsernamePasswordAuthenticationToken(
new TestPrincipal('testuser1'), 'password1'
)
shouldFail(BadCredentialsException) {
daoAuthenticationProvider.authenticate(token)
}
}
void testAuthenticationSucceedsWithCorrectPassword() {
def user = new User(
username: 'testuser1', password: 'password', enabled: true
).save(flush: true)
def token = new UsernamePasswordAuthenticationToken(
new TestPrincipal('testuser1'), 'password'
)
def result = daoAuthenticationProvider.authenticate(token)
assertTrue result.authenticated
}
class TestPrincipal implements Principal {
String name
TestPrincipal(def name) {
this.name = name
}
boolean equals(Object o) {
if (name == null)
return o == null
return name.equals(o)
}
int hashCode() {
return toString().hashCode()
}
String toString() {
return String.valueOf(name)
}
}
}
And now run your integration tests:
$ grails test-app integration:
The console outputs only a brief overview of the results. If something went wrong, you can find the details in the target/test-reports
folder; enter the following in your browser address bar for the html version of the report (where $PROJECT_HOME
is the full path to your project):
file://$PROJECT_HOME/target/test-reports/html/index.html
And hey presto, you've got salt.