Grails 4 and Spring Security Custom User Details Example

by Didin J. on Jun 25, 2020 Grails 4 and Spring Security Custom User Details Example

A comprehensive step by step Grails 4 tutorial on creating custom Spring Security custom user details

In this tutorial, we will show you how to implementing custom user details for Grails 4 and Spring Security web applications. As a default, the Grails 4 Spring Security generated user domain or entity has only username and role fields that can get or show in the SecurityTagLib. So, we will add additional fields to display user info or details in the SecurityTagLib.

This tutorial divided into several steps:

The following tools, frameworks, and libraries are required for this tutorial:

  1. Grails 4 (the latest stable version)
  2. Spring Security Plugin
  3. Terminal or CMD
  4. Text Editor or IDE (We are using VSCode)

Let's get started with the main steps!


Step #1: Create a New Grails 4 Application

Before creating a new Grails 4 application, make sure that you have installed the latest Grails 4 version in your machine and can run the Grails 4 interactive console in the terminal or CMD. In the Mac terminal, we will use SDKman to install the latest Grails 4. To see the available Grails version in the SDKman, type this command.

sdk list grails

As you can see, there is some version of Grails 4 but we will install and use the stable version of Grails 4 by type this command.

sdk i grails 4.0.3
sdk u grails 4.0.3

Next, create a new Grails 4 application by type this command.

grails create-app securepage

Go to the newly created Grail 4 application. Then open the Grails project with your IDE or Text Editor.

cd securepage
code .

To working with Grails interactive console, type this command.

grails

If you see this error.

Error Error initializing classpath: Timeout of 120000 reached waiting for exclusive access to file: /Users/didin/.gradle/wrapper/dists/gradle-5.1.1-bin/90y9l8txxfw1s2o6ctiqeruwn/gradle-5.1.1-bin.zip (Use --stacktrace to see the full trace)

Just, remove .grails folder in the home directory then start again Grails interactive console.

rm -rf ~/.gradle
grails

For sanitation, start this Grails 4 application by type this command inside Grails interactive console.

run-app

Open your browser then go to "localhost:8080" and you should see this Grails page.

Grails 4 and Spring Security Custom User Details Example - Grails Home

To stop or shutdown the running Grails server, type this command.

stop-app


Step #2: Add Spring Security Plugin

To add the Grails Spring Security Plugin, open and edit "build.gradle" in the root of the project folder. Then add this line to the dependencies.

dependencies {
...
    compile 'org.grails.plugins:spring-security-core:4.0.0'
...
}

Compile this project to download and install the Spring Security dependency.

compile


Step #3: Generate User and Role Domain

Still, on Grails interactive console, type this command to generate the User and Role domain class.

s2-quickstart com.djamware.securepage User Role

That command generates the User.groovy, Role.groovy, and UserRole.groovy domain class. Also, an additional configuration file "grails-app/conf/application.groovy". The generated   User.groovy look like this.

package com.djamware.securepage

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable {

    private static final long serialVersionUID = 1

    String username
    String password
    boolean enabled = true
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    Set<Role> getAuthorities() {
        (UserRole.findAllByUser(this) as List<UserRole>)*.role as Set<Role>
    }

    static constraints = {
        password nullable: false, blank: false, password: true
        username nullable: false, blank: false, unique: true
    }

    static mapping = {
        password column: '`password`'
    }
}

The generated Role.groovy looks like this.

package com.djamware.securepage

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Role implements Serializable {

    private static final long serialVersionUID = 1

    String authority

    static constraints = {
        authority nullable: false, blank: false, unique: true
    }

    static mapping = {
        cache true
    }
}

The generated UserRole.groovy looks like this.

package com.djamware.securepage

import grails.gorm.DetachedCriteria
import groovy.transform.ToString

import org.codehaus.groovy.util.HashCodeHelper
import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserRole implements Serializable {

    private static final long serialVersionUID = 1

    User user
    Role role

    @Override
    boolean equals(other) {
        if (other instanceof UserRole) {
            other.userId == user?.id && other.roleId == role?.id
        }
    }

    @Override
    int hashCode() {
        int hashCode = HashCodeHelper.initHash()
        if (user) {
            hashCode = HashCodeHelper.updateHash(hashCode, user.id)
        }
        if (role) {
            hashCode = HashCodeHelper.updateHash(hashCode, role.id)
        }
        hashCode
    }

    static UserRole get(long userId, long roleId) {
        criteriaFor(userId, roleId).get()
    }

    static boolean exists(long userId, long roleId) {
        criteriaFor(userId, roleId).count()
    }

    private static DetachedCriteria criteriaFor(long userId, long roleId) {
        UserRole.where {
            user == User.load(userId) &&
            role == Role.load(roleId)
        }
    }

    static UserRole create(User user, Role role, boolean flush = false) {
        def instance = new UserRole(user: user, role: role)
        instance.save(flush: flush)
        instance
    }

    static boolean remove(User u, Role r) {
        if (u != null && r != null) {
            UserRole.where { user == u && role == r }.deleteAll()
        }
    }

    static int removeAll(User u) {
        u == null ? 0 : UserRole.where { user == u }.deleteAll() as int
    }

    static int removeAll(Role r) {
        r == null ? 0 : UserRole.where { role == r }.deleteAll() as int
    }

    static constraints = {
        user nullable: false
        role nullable: false, validator: { Role r, UserRole ur ->
            if (ur.user?.id) {
                if (UserRole.exists(ur.user.id, r.id)) {
                    return ['userRole.exists']
                }
            }
        }
    }

    static mapping = {
        id composite: ['user', 'role']
        version false
    }
}

You don't have to worry about login page view, it automatically handles by the Spring Security. 


Step #4: Create a Custom User Details

Now, we will add additional fields to the User domain class. Open and edit "grails-app/domain/com/djamware/securepage/User.groovy" then add this fields after another fields.

    String fullname
    String address

Also, add these constraints.

    static constraints = {
        ...
        fullname nullable: false, blank: false
        address nullable: false, blank: false
    }

Next, create a Groovy file "src/main/groovy/com/djamware/securepage/CustomUserDetails.groovy". On this file, add these package and imports.

package com.djamware.securepage

import grails.plugin.springsecurity.userdetails.GrailsUser
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User

Then add the CustomUserDetails class that adds full name and address fields.

class CustomUserDetails extends GrailsUser {

    final String fullname
    final String address

    CustomUserDetails(String username, String password, boolean enabled,
                    boolean accountNonExpired, boolean credentialsNonExpired,
                    boolean accountNonLocked,
                    Collection<GrantedAuthority> authorities,
                    long id, String fullname, String address) {
        super(username, password, enabled, accountNonExpired,
                credentialsNonExpired, accountNonLocked, authorities, id)

        this.fullname = fullname
        this.address= address
    }
}

Next, generate a CustomUserDetailsService by type this command in Grails interactive console.

create-service com.djamware.securepage.CustomUserDetails

Open and edit "grails-app/services/com/djamware/securepage/CustomUserDetailsService.groovy" then add or replace these imports.

import grails.plugin.springsecurity.SpringSecurityUtils
import grails.plugin.springsecurity.userdetails.GrailsUserDetailsService
import grails.plugin.springsecurity.userdetails.NoStackUsernameNotFoundException
import grails.gorm.transactions.Transactional
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException

Remove the @Trannsactional annotation and add implements GrailsUserDetailsService.

class CustomUserDetailsService implements GrailsUserDetailsService {
    ...
}

Replace the class body with this.

    static final List NO_ROLES = [new SimpleGrantedAuthority(SpringSecurityUtils.NO_ROLE)]

    UserDetails loadUserByUsername(String username, boolean loadRoles)
            throws UsernameNotFoundException {
        return loadUserByUsername(username)
    }

    @Transactional(readOnly=true, noRollbackFor=[IllegalArgumentException, UsernameNotFoundException])
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = User.findByUsername(username)
        if (!user) throw new NoStackUsernameNotFoundException()

        def roles = user.authorities

        def authorities = roles.collect {
            new SimpleGrantedAuthority(it.authority)
        }

        return new CustomUserDetails(user.username, user.password, user.enabled,
            !user.accountExpired, !user.passwordExpired,
            !user.accountLocked, authorities ?: NO_ROLES, user.id,
            user.fullname, user.address)
    }

Inject that service to the "grails-app/conf/spring/resources.groovy".

import com.djamware.securepage.UserPasswordEncoderListener
import com.djamware.securepage.CustomUserDetailsService

// Place your Spring DSL code here
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener)
    userDetailsService(CustomUserDetailsService)
}


Step #5: Add a Secure Page

We will create a single secure page by creating a controller first. Type this command to create a controller from Grails interactive console.

create-controller com.djamware.securepage.Dashboard

Open and edit "grails-app/controllers/com/djamware/securepage/Dashboard.groovy" then add this import.

import grails.plugin.springsecurity.annotation.Secured

Add @Secured annotation before the class name.

@Secured('ROLE_ADMIN')
class DashboardController {
...
}

Next, create a new GSP file "grails-app/views/dashboard/index.gsp" then replace all GSP tags with this.

<!doctype html>
<html>
    <head>
        <meta name="layout" content="main"/>
        <title>Secure Dashboard</title>
    </head>
    <body>
        <div id="content" role="main">
            <section class="row colset-2-its">
                <sec:ifLoggedIn>
                    <h1>Welcome to the secure dashboard <sec:loggedInUserInfo field='fullname'/>!</h1>
                    <p><sec:loggedInUserInfo field='address'/></p>
                    <h2><g:link controller="logout">Logout</g:link></h2>
                </sec:ifLoggedIn>
            </section>
        </div>
    </body>
</html>

As a default logout method only working with the POST method only. To make it available using the GET method, open and edit "grails-app/conf/application.groovy" then add this line.

grails.plugin.springsecurity.logout.postOnly = false


Step #6: Run and Test Grails 4 Spring Security Application

For testing purposes, we will create a dummy user and role by adding this import to "grails-app/init/securepage/Bootstrap.groovy".

import com.djamware.securepage.*

Add these lines to the init method to create a new role, user, and userRole.

   def init = { servletContext ->
        def adminRole
        Role.withTransaction { rl ->
            adminRole = new Role(authority: 'ROLE_ADMIN').save(flush: true)
        }

        def testUser
        User.withTransaction { us ->
            testUser = new User(username: 'jacksparrow', password: 'password1', fullname: 'Jack Sparrow', address: '1600 Amphitheatre Parkway, Mountain View, California, United States').save(flush: true)
        }

        UserRole.create testUser, adminRole

        UserRole.withTransaction { urole ->
            UserRole.withSession {
                it.flush()
                it.clear()
            }
        }
    }

Next, run again this Grails 4 application from the Grails interactive console.

run-app

And here's the Grails 4 Spring Security application look like.

Grails 4 and Spring Security Custom User Details Example - Demo 1
Grails 4 and Spring Security Custom User Details Example - Demo 2

That it's, the Grails 4 and Spring Security Custom User Details Example. You can get the full source code in our GitHub.

That just the basic. If you need more deep learning about Groovy and Grails you can take the following cheap course:

Thanks!