Grails with Spring Security: How do I keep a password history to prevent password reuse

29 Views Asked by At

For compliance reasons, we need to prevent users from reusing their passwords. I found this Q&A, which is very, very helpful, but I can't find an example implementation for a Grails application.

In our app, a user's password may be reset in two ways: an "edit user" view visible to admins and a self-service password reset flow. Ideally, I would like to perform this check during the Spring Security password encoding step to ensure it is applied regardless of which user action changes the password.

What's the best way to do that in Grails?

1

There are 1 best solutions below

0
SGT Grumpy Pants On

Answering my own question here:

Short answer:

I found the personPasswordEncoderListener defined in resources.groovy referenced a PersonPasswordEncoderListener class in src/main/groovy. That's where I needed to insert my logic.

Full answer:

First, I added a Password class to record the password history for each user

package com.company

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode(includes = ["person", "hash"])
class Password {
    Person person
    String hash
    Date dateCreated

    static belongsTo = Person

    static constraints = {
        hash unique: "person"
    }

    static mapping = {
        version(false)
    }
}

Then, a PasswordDataService to encapsulate persistence logic.

package com.company

import grails.gorm.services.Service

@Service(Password)
interface PasswordDataService {
    Password get(Serializable id)

    List<Password> findAllByPerson(Person person)

    Password save(Password password)
}

Then, I added the PasswordService to handle the business logic.

package com.company

import grails.gorm.transactions.Transactional
import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.security.crypto.password.PasswordEncoder
import com.company.exception.InvalidResourceException

@Transactional
class PasswordService {

    MessageSource messageSource
    PasswordDataService passwordDataService
    PasswordEncoder passwordEncoder

    void preventPasswordReuse(Person person) {
        findAllByPerson(person).each { password ->
            if (passwordEncoder.matches(person.password, password.hash)) {
                String msg = messageSource.getMessage("passwordService.exception.passwordReused", [] as Object[], "You cannot use a password you have used before", LocaleContextHolder.locale)
                throw new InvalidResourceException(msg)
            }
        }
    }

    void recordPassword(Person person) {
        save(new Password(person: person, hash: person.password))
    }

    /* *** Data Service Pass-through Methods *** */

    Password get(Long id) {
        passwordDataService.get(id)
    }

    List<Password> findAllByPerson(Person person) {
        passwordDataService.findAllByPerson(person)
    }

    Password save(Password password) {
        passwordDataService.save(password)
    }
}

Then, I added calls to the PasswordService in PersonPasswordEncoderListener.encodePasswordForEvent(..). The vast majority of PersonPasswordEncoderListener was already written (I only added 4 lines), but I'll include it in its entirety here for context and annotate the new lines with //added

package com.company

import grails.events.annotation.gorm.Listener
import grails.plugin.springsecurity.SpringSecurityService
import groovy.transform.CompileStatic
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.beans.factory.annotation.Autowired

@CompileStatic
class PersonPasswordEncoderListener {

    @Autowired
    SpringSecurityService springSecurityService

    @Autowired //added
    PasswordService passwordService //added

    @Listener(Person)
    void onPreInsertEvent(PreInsertEvent event) {
        encodePasswordForEvent(event)
    }

    @Listener(Person)
    void onPreUpdateEvent(PreUpdateEvent event) {
        encodePasswordForEvent(event)
    }

    private void encodePasswordForEvent(AbstractPersistenceEvent event) {
        if (event.entityObject instanceof Person) {
            Person person = event.entityObject as Person
            if (person.password && ((event instanceof PreInsertEvent) || (event instanceof PreUpdateEvent && person.isDirty('password')))) {
                passwordService.preventPasswordReuse(person) //added
                event.getEntityAccess().setProperty('password', encodePassword(person.password))
                passwordService.recordPassword(person) //added
            }
        }
    }

    private String encodePassword(String password) {
        springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}

This created a problem because Spring Security wrapped the InvalidResourceException that I was throwing in PasswordService.preventPasswordReuse in an UndeclaredThrowableException, which hid the exception message I needed to pass along to my view. I resolved this by adding an exception handler to the exception handling trait that all our controllers implement to uncover the cause of the UndeclaredThrowableException.

    def defaultExceptionHandler(Exception e) {
        ... pre-existing exception handler code removed ...
    }

    def undeclaredThrowableException(final UndeclaredThrowableException e) {
        defaultExceptionHandler(e?.cause as Exception ?: e)
    }