Search code examples
grailsgrails-ormgorm-mongodbgrails-spring-security

Grails MongoDB Dirty Checking Fails With Spring Security


I'm using Grails 3.3.2 with the mongoDB plugin (v6.1.4) and Spring Security Core plugin (v3.2.0).

I have the following UserPasswordEncoderListenerwith the following persistenceEvent method:

 @Override
protected void onPersistenceEvent(AbstractPersistenceEvent event) {
    if (event.entityObject instanceof User) {
        User u = (event.entityObject as User)
        if (u.password && (event.eventType == EventType.PreInsert || (event.eventType == EventType.PreUpdate && u.hasChanged('password')))) {
            event.getEntityAccess().setProperty("password", encodePassword(u.password))
        }
    }
}

The problem is the hasChanged call always return true every time I save a user object that has NO updates causing an already encoded password to be re-encoded and thereby breaking authentication.

One workaround will be to do it the old way and just retrieve the original password from the db and compare them before encoding but I'm wondering why hasChanged is falsely returning true.

I tested that hasChanged behaves correctly elsewhere by running the following in the groovy console:

def user = User.findByEmail("[email protected]")
println "Result: "+ user.hasChanged('password')

The result is Result: false. Why does it NOT work in the persistence listener class?

FYI: I have the following bean defined in resources.groovy:

userPasswordEncoderListener(UserPasswordEncoderListener,ref('mongoDatastore'))

Solution

  • Have you tried using isDirty() rather than hasChanged() in your listener?

    For example:

    package com.mycompany.myapp
    
    import grails.plugin.springsecurity.SpringSecurityService
    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
    import grails.events.annotation.gorm.Listener
    import groovy.transform.CompileStatic
    
    @CompileStatic
    class UserPasswordEncoderListener {
    
        @Autowired
        SpringSecurityService springSecurityService
    
        @Listener(User)
        void onPreInsertEvent(PreInsertEvent event) {
            encodePasswordForEvent(event)
        }
    
        @Listener(User)
        void onPreUpdateEvent(PreUpdateEvent event) {
            encodePasswordForEvent(event)
        }
    
        private void encodePasswordForEvent(AbstractPersistenceEvent event) {
            if (event.entityObject instanceof User) {
                User u = event.entityObject as User
                if (u.password && ((event instanceof  PreInsertEvent) || (event instanceof PreUpdateEvent && u.isDirty('password')))) {
                    event.getEntityAccess().setProperty('password', encodePassword(u.password))
                }
            }
        }
    
        private String encodePassword(String password) {
            springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
        }
    }
    

    More info is available at: https://grails-plugins.github.io/grails-spring-security-core/3.2.x/index.html#tutorials