Search code examples
grailsgrails-plugingrails-controller

grails - spring security UI strange behaviour


I'm trying to customize spring security ui plugin. What I want to achieve:

1) admin creates a new user with email from spring security ui

2) app generates link and send it to user's email

3) user clicks on the link and sees page where he can specify password and save it

4) after user submits page account become active with password that he specified

for this purpose I've decided to customize UserController that is provided by spring security UI by default:

class UserController extends grails.plugin.springsecurity.ui.UserController {

    def mailService

    def save() {
        def user = lookupUserClass().newInstance(params)
        if (!user.save(flush: true)) {
            render view: 'create', model: [user: user, authorityList: sortedRoles()]
            return
        }

        //generate secured link
        def registrationCode = new RegistrationCode(username: user.username).save(flush: true)
        String url = generateLink('verifyRegistration', [t: registrationCode.token])
        def conf = SpringSecurityUtils.securityConfig

        //here we should send an invitation mail
        mailService.sendMail {
            async true
            to user.email
            from conf.ui.register.emailFrom
            subject conf.ui.register.emailSubject
            html url
        }

        addRoles(user)
        flash.message = "${message(code: 'default.created.message', args: [message(code: 'user.label', default: 'User'), user.id])}"
        redirect action: 'edit', id: user.id
    }



    def verifyRegistration() {
        def conf = SpringSecurityUtils.securityConfig
        String defaultTargetUrl = conf.successHandler.defaultTargetUrl
        String token = params.t
        println token

        def registrationCode = token ? RegistrationCode.findByToken(token) : null
        if (!registrationCode) {
            println registrationCode
            flash.error = message(code: 'spring.security.ui.register.badCode')
            redirect uri: defaultTargetUrl
            return
        }

        render(view: "initpassword", model: [token: token])
    }

    def savePassword() {
        String token = params.token
        def conf = SpringSecurityUtils.securityConfig
        String defaultTargetUrl = conf.successHandler.defaultTargetUrl
        def registrationCode = token ? RegistrationCode.findByToken(token) : null

        //TODO check if passwords are equal

        if (!registrationCode) {
            flash.error = message(code: 'spring.security.ui.register.badCode')
            redirect uri: defaultTargetUrl
            return
        }

        def user

        RegistrationCode.withTransaction { status ->
            String usernameFieldName = SpringSecurityUtils.securityConfig.userLookup.usernamePropertyName
            user = lookupUserClass().findWhere((usernameFieldName): registrationCode.username)
            if (!user) {
                return
            }
            user.accountLocked = false

            String salt = saltSource instanceof NullSaltSource ? null : params.username
            user.password = springSecurityUiService.encodePassword(params.password1, salt)
            user.save(flush: true)
            def UserRole = lookupUserRoleClass()
            def Role = lookupRoleClass()
            for (roleName in conf.ui.register.defaultRoleNames) {
                UserRole.create user, Role.findByAuthority(roleName)
            }
            registrationCode.delete()
        }

        if (!user) {
            flash.error = message(code: 'spring.security.ui.register.badCode')
            redirect uri: defaultTargetUrl
            return
        }

    }
}

and initpassword.gsp is very simple and looks like this:

<g:form action="savePassword" absolute="true" method="POST">
    <g:passwordField name="password1"/>
    <g:passwordField name="password2"/>
    <g:hiddenField value="${token}" name="token"/>
    <g:actionSubmit value="Save"/>
</g:form>

Everything looks good so far (at least for me, please, let me know if you've observed smth wrong) but very strange thing happens when I try to test it:

1) I click on the link that was sent in email (verifyRegistration() method is invoked)

2) app shows this page where I can type password (initpassword page is shown)

3) I submit the page (here savePassword() should be invoked but for some reason its not invoked) and app shows me default page for creating a new user from spring security ui plugin with validation erros where it said that fields for user (all one by one) can't be empty

4) When I try to debug (with breakpoint in the begining of savePassword() method) nothing happens - it seems like controller just don't catch request to savePassword()

But when I try to just open savePassword() method from browser (typing localhost/myApp/user/savePassword) controller catches the request and debugging on that breakpoint works.

I've spent almost 4 hours dealing with this. Have no idea what actually going on. Some magic with Grails. Can someone help me with this? Thank you!!!


Solution

  • In your initpassword.gsp just add the action="savePassword" attribute to the g:actionSubmit tag. You even may delete this attribute from the g:form tag. That is Grails ability to have multiple actions in form. I'm not sure but in clean HTML it may work too.

    <html>
    <head>
        <title></title>
    </head>
    
    <body>
    <g:form controller="user" absolute="true" method="POST">
        <g:passwordField name="password1"/>
        <g:passwordField name="password2"/>
        <g:hiddenField value="${token}" name="token"/>
        <g:actionSubmit value="Save" action="savePassword"/>
    </g:form>
    </body>
    </html>