Show Navigation

Testing a Secured Grails Application

In this guide you will see how to test the security constraints you added with Grails Spring Security REST Plugin and Grails Spring Security Core Plugin.

Authors: Sergio del Amo

Grails Version: 3.3.0

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide you are going to:

  • Create a functional test of a REST API endpoint secured with the Grails Spring Security Rest Plugin

  • Create a GebSpec Functional test of a page secured with the Grails Spring Security Core Plugin. When you try to access such a page you are redirected to a sign-in form.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with some additional code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-test-security/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-test-security/complete

3 Writing the Application

We are going to write an app which combines both a traditional web application and an API. API endpoints will be prefixed with /api/

3.1 Domain Class

We’ll create a Domain Class, Announcement, which we are going to use as an example through this guide:

./grailsw create-domain-class Announcement
| Created grails-app/domain/grails/test/security/Announcement.groovy
| Created src/test/groovy/grails/test/security/AnnouncementSpec.groovy
grails-app/domain/grails/test/security/Announcement.groovy
package grails.test.security

import groovy.transform.CompileStatic

@CompileStatic
class Announcement {

    String message

    static constraints = {
    }
}

3.2 Web Controller

We are going to use static Scaffolding to generate a controller for the web app.

./grailsw generate-all Announcement
| Rendered template Controller.groovy to destination grails-app/controllers/grails/test/security/AnnouncementController.groovy
| Rendered template Spec.groovy to destination src/test/groovy/grails/test/security/AnnouncementControllerSpec.groovy
| Scaffolding completed for grails-app/domain/grails/test/security/Announcement.groovy
| Rendered template edit.gsp to destination grails-app/views/announcement/edit.gsp
| Rendered template create.gsp to destination grails-app/views/announcement/create.gsp
| Rendered template index.gsp to destination grails-app/views/announcement/index.gsp
| Rendered template show.gsp to destination grails-app/views/announcement/show.gsp
| Views generated for grails-app/domain/grails/test/security/Announcement.groovy

3.3 Api Controller

To create the Announcement API controller we’ll use the create-controller command

./grailsw create-controller ApiAnnouncement
Created grails-app/controllers/grails/test/security/ApiAnnouncementController.groovy
| Created src/test/groovy/grails/test/security/ApiAnnouncementControllerSpec.groovy

We want this controller to handle any requests to /api/announcements. We’ll need to add the following line to our UrlMappings.

grails-app/controllers/grails/test/security/UrlMappings.groovy
'/api/announcements'(controller: 'apiAnnouncement')

The controller will be a RestfulController, and will only respond with JSON.

grails-app/controllers/grails/test/security/ApiAnnouncementController.groovy
package grails.test.security

import grails.rest.RestfulController
import groovy.transform.CompileStatic

@CompileStatic
class ApiAnnouncementController extends RestfulController {
    static responseFormats = ['json']

    ApiAnnouncementController() {
        super(Announcement)
    }
}

3.4 Securing the App

Add a dependency on the Grails Spring Security Rest plugin and to the latest release of Spring Security Core to our build.gradle file.

/build.gradle
    compile ("org.grails.plugins:spring-security-rest:2.0.0.M2")
    compile 'org.grails.plugins:spring-security-core:3.2.0.M1'

We’ll run the s2-quickstart command, provided by the Spring Security Core Plugin, to generate User and Authority classes.

./grailsw s2-quickstart grails.test.security User SecurityRole

s2-quickstart script generates three domain classes; User, SecurityRole and UserSecurityRole.

grails-app/domain/grails/test/security/User.groovy
package grails.test.security

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<SecurityRole> getAuthorities() {
                (UserSecurityRole.findAllByUser(this) as List<UserSecurityRole>)*.securityRole as Set<SecurityRole>
        }

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

        static mapping = {
                password column: '`password`'
        }
}
grails-app/domain/grails/test/security/SecurityRole.groovy
package grails.test.security

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

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

        private static final long serialVersionUID = 1
        String authority

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

        static mapping = {
                cache true
        }
}
grails-app/domain/grails/test/security/UserSecurityRole.groovy
package grails.test.security

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

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

@SuppressWarnings(['FactoryMethodName', 'Instanceof'])
@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserSecurityRole implements Serializable {

        private static final long serialVersionUID = 1

        User user
        SecurityRole securityRole

        @Override
        boolean equals(other) {
                if (other instanceof UserSecurityRole) {
                        other.userId == user?.id && other.securityRoleId == securityRole?.id
                }
        }

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

        static UserSecurityRole get(long userId, long securityRoleId) {
                criteriaFor(userId, securityRoleId).get()
        }

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

        private static DetachedCriteria criteriaFor(long userId, long securityRoleId) {
                UserSecurityRole.where {
                        user == User.load(userId) &&
                                        securityRole == SecurityRole.load(securityRoleId)
                }
        }

        static UserSecurityRole create(User user, SecurityRole securityRole, boolean flush = false) {
                def instance = new UserSecurityRole(user: user, securityRole: securityRole)
                instance.save(flush: flush)
                instance
        }

        static boolean remove(User u, SecurityRole r) {
                if (u != null && r != null) {
                        UserSecurityRole.where { user == u && securityRole == r }.deleteAll()
                }
        }

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

        static int removeAll(SecurityRole r) {
                r == null ? 0 : UserSecurityRole.where { securityRole == r }.deleteAll() as int
        }

        static constraints = {
                securityRole validator: { SecurityRole r, UserSecurityRole ur ->
                        if (ur.user?.id) {
                                UserSecurityRole.withNewSession {
                                        if (UserSecurityRole.exists(ur.user.id, r.id)) {
                                                return ['userRole.exists']
                                        }
                                }
                        }
                }
        }

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

If you are using GORM 6.0.10 or a later version and Spring Security core 3.1.2 or later version, s2-quickstart generates and registers a bean to handle password encoding:

src/main/groovy/grails/test/security/UserPasswordEncoderListener.groovy
package grails.test.security

import grails.plugin.springsecurity.SpringSecurityService
import org.grails.datastore.mapping.core.Datastore
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
import org.grails.datastore.mapping.engine.event.AbstractPersistenceEventListener
import org.grails.datastore.mapping.engine.event.EventType
import org.grails.datastore.mapping.engine.event.PreInsertEvent
import org.grails.datastore.mapping.engine.event.PreUpdateEvent
import org.springframework.context.ApplicationEvent
import org.springframework.beans.factory.annotation.Autowired
import groovy.transform.CompileStatic

@SuppressWarnings(['UnnecessaryGetter', 'LineLength', 'Instanceof'])
@CompileStatic
class UserPasswordEncoderListener extends AbstractPersistenceEventListener {

    @Autowired
    SpringSecurityService springSecurityService

    UserPasswordEncoderListener(final Datastore datastore) {
        super(datastore)
    }

    @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.isDirty('password')))) {
                event.getEntityAccess().setProperty('password', encodePassword(u.password))
            }
        }
    }

    @Override
    boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        eventType == PreUpdateEvent || eventType == PreInsertEvent
    }

    private String encodePassword(String password) {
        springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}
grails-app/conf/spring/resources.groovy
import grails.test.security.UserPasswordEncoderListener
// Place your Spring DSL code here
beans = {
    userPasswordEncoderListener(UserPasswordEncoderListener, ref('hibernateDatastore'))
}

We’ll configure the security rules of the application in the file application.groovy, as show below. We have one stateless chain and one Traditional Chain.

grails-app/conf/application.groovy
grails {
    plugin {
        springsecurity {
            securityConfigType = "InterceptUrlMap"  (1)
            filterChain {
                chainMap = [
                [pattern: '/api/**',filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'],(2)
                [pattern: '/**', filters: 'JOINED_FILTERS,-restTokenValidationFilter,-restExceptionTranslationFilter'] (3)
                ]
            }
            userLookup {
                userDomainClassName = 'grails.test.security.User' (4)
                authorityJoinClassName = 'grails.test.security.UserSecurityRole' (4)
            }
            authority {
                className = 'grails.test.security.SecurityRole' (4)
            }
            interceptUrlMap = [
                    [pattern: '/',                      access: ['permitAll']],
                    [pattern: '/error',                 access: ['permitAll']],
                    [pattern: '/index',                 access: ['permitAll']],
                    [pattern: '/index.gsp',             access: ['permitAll']],
                    [pattern: '/shutdown',              access: ['permitAll']],
                    [pattern: '/assets/**',             access: ['permitAll']],
                    [pattern: '/**/js/**',              access: ['permitAll']],
                    [pattern: '/**/css/**',             access: ['permitAll']],
                    [pattern: '/**/images/**',          access: ['permitAll']],
                    [pattern: '/**/favicon.ico',        access: ['permitAll']],
                    [pattern: '/login/**',              access: ['permitAll']], (5)
                    [pattern: '/logout',                access: ['permitAll']],
                    [pattern: '/logout/**',             access: ['permitAll']],
                    [pattern: '/announcement',          access: ['ROLE_BOSS', 'ROLE_EMPLOYEE']],
                    [pattern: '/announcement/index',    access: ['ROLE_BOSS', 'ROLE_EMPLOYEE']],  (6)
                    [pattern: '/announcement/create',   access: ['ROLE_BOSS']],
                    [pattern: '/announcement/save',     access: ['ROLE_BOSS']],
                    [pattern: '/announcement/update',   access: ['ROLE_BOSS']],
                    [pattern: '/announcement/delete/*', access: ['ROLE_BOSS']],
                    [pattern: '/announcement/edit/*',   access: ['ROLE_BOSS']],
                    [pattern: '/announcement/show/*',   access: ['ROLE_BOSS', 'ROLE_EMPLOYEE']],
                    [pattern: '/api/login',             access: ['ROLE_ANONYMOUS']], (7)
                    [pattern: '/oauth/access_token',    access: ['ROLE_ANONYMOUS']], (8)
                    [pattern: '/api/announcements',     access: ['ROLE_BOSS'], httpMethod: 'GET'],  (9)
                    [pattern: '/api/announcements/*',   access: ['ROLE_BOSS'], httpMethod: 'GET'],
                    [pattern: '/api/announcements/*',   access: ['ROLE_BOSS'], httpMethod: 'DELETE'],
                    [pattern: '/api/announcements',     access: ['ROLE_BOSS'], httpMethod: 'POST'],
                    [pattern: '/api/announcements/*',   access: ['ROLE_BOSS'], httpMethod: 'PUT']
            ]
        }
    }
}
1 We choose to configure security with a InterceptUrlMap
2 Stateless Chain
3 Traditional chain
4 Classes generated by the s2-quickstart script
5 URL of login page
6 Url accesible only for users authenticated and with role ROLE_BOSS or ROLE_EMPLOYEE
7 Spring Security Rest for Grails default Authentication Endpoint. It should allow anonymous access
8 Spring Security Rest for Grails default Refresh Token Endpoint. It should allow anonymous access
9 Url accesible only for users authenticated and with role ROLE_BOSS

We’ll populate our database in BootStrap, inserting two users when the application starts.

grails-app/init/grails/test/security/BootStrap.groovy
package grails.test.security

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BootStrap {

    def init = { servletContext ->

        def authorities = ['ROLE_BOSS', 'ROLE_EMPLOYEE']
        authorities.each { String authority ->
            if ( !SecurityRole.findByAuthority(authority) ) {
                new SecurityRole(authority: authority).save()
            }
        }

        if ( !User.findByUsername('sherlock') ) {
            def u = new User(username: 'sherlock', password: 'elementary')
            u.save()
            new UserSecurityRole(user: u, securityRole: SecurityRole.findByAuthority('ROLE_BOSS')).save()
        }

        if ( !User.findByUsername('watson') ) {
            def u = new User(username: 'watson', password: '221Bbakerstreet')
            u.save()
            new UserSecurityRole(user: u, securityRole: SecurityRole.findByAuthority('ROLE_EMPLOYEE')).save()
        }
    }
    def destroy = {
    }
}

3.5 Test Rest Api

To test REST part of the application, we use Rest Client Builder Grails Plugin. We need to add the dependency:

/build.gradle
    testCompile "org.grails:grails-datastore-rest-client"

Our first test will verify that the /api/announcements endpoint is only accessible to users with role ROLE_BOSS

/src/integration-test/groovy/grails/test/security/ApiAnnouncementControllerSpec.groovy
package grails.test.security

import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import grails.transaction.Rollback
import spock.lang.Specification

@SuppressWarnings(['MethodName', 'DuplicateNumberLiteral'])
@Integration
@Rollback
class ApiAnnouncementControllerSpec extends Specification {

    def 'test /api/announcements url is secured'() {
        given:
        RestBuilder rest = new RestBuilder()

        when:
        def resp = rest.get("http://localhost:${serverPort}/api/announcements") { (1)
            accept('application/json') (2)
            contentType('application/json') (3)
        }

        then:
        resp.status == 401 (4)
        resp.json.status == 401
        resp.json.error == 'Unauthorized'
        resp.json.message == 'No message available'
        resp.json.path == '/api/announcements'
    }

    def "test a user with the role ROLE_BOSS is able to access /api/announcements url"() {
        when: 'login with the sherlock'
        RestBuilder rest = new RestBuilder()
        def resp = rest.post("http://localhost:${serverPort}/api/login") { (5)
            accept('application/json')
            contentType('application/json')
            json {
                username = 'sherlock'
                password = 'elementary'
            }
        }

        then:
        resp.status == 200
        resp.json.roles.find { it == 'ROLE_BOSS' }

        when:
        def accessToken = resp.json.access_token

        then:
        accessToken

        when:
        resp = rest.get("http://localhost:${serverPort}/api/announcements") {
            accept('application/json')
            header('Authorization', "Bearer ${accessToken}") (6)
        }

        then:
        resp.status == 200 (7)
    }

    def "test a user with the role ROLE_EMPLOYEE is NOT able to access /api/announcements url"() {
        when: 'login with the watson'
        RestBuilder rest = new RestBuilder()

        def resp = rest.post("http://localhost:${serverPort}/api/login") {
            accept('application/json')
            contentType('application/json')
            json {
                username = 'watson'
                password = '221Bbakerstreet'
            }
        }

        then:
        resp.status == 200
        !resp.json.roles.find { it == 'ROLE_BOSS' }
        resp.json.roles.find { it == 'ROLE_EMPLOYEE' }

        when:
        def accessToken = resp.json.access_token

        then:
        accessToken

        when:
        resp = rest.get("http://localhost:${serverPort}/api/announcements") {
            accept('application/json')
            header('Authorization', "Bearer ${accessToken}")
        }

        then:
        resp.status == 403 (8)

    }
}
1 serverPort property is automatically injected and it contains the random port where the app will be running for the functional test.
2 If you want JSON in the response, you should configure the Accept header to indicate you expect a JSON response.
3 When you are submitting a JSON payload in the request body, you should set the Content-Type header appropriately.
4 The server returns a 401, indicating that the resource is secured.
5 Call the authentication endpoint
6 We pass the access_token we obtained from the authentication endpoint in the request headers.
7 An authenticated user with the role ROLE_BOSS can access the resource.
8 An authenticated user but without the role ROLE_BOSS is not allowed to access the resource.

3.6 Test Web Endpoint

Now we are going to execute a Functional Test using the Geb framework.

To make this test easier to read and maintain we’ve created two Geb Pages: a LoginPage and an AnnouncementListingPage.

/src/integration-test/groovy/grails/test/security/LoginPage.groovy
package grails.test.security

import geb.Page

class LoginPage extends Page {
    static url = '/login/auth'

    static at = {
        title == 'Login'
    }

    static content = {
        loginButton { $('#submit', 0) }
        usernameInputField { $('#username', 0) }
        passwordInputField { $('#password', 0) }
    }

    void login(String username, String password) {
        usernameInputField << username
        passwordInputField << password
        loginButton.click()
    }
}
/src/integration-test/groovy/grails/test/security/AnnouncementListingPage.groovy
package grails.test.security

import geb.Page

class AnnouncementListingPage extends Page {
    static url = '/announcement/index'

    static at = {
        $('#list-announcement').text()?.contains 'Announcement List'
    }
}
/src/integration-test/groovy/grails/test/security/AnnouncementControllerSpec.groovy
package grails.test.security

import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration

@SuppressWarnings('MethodName')
@Integration
class AnnouncementControllerSpec extends GebSpec {

    void 'test /announcement/index is secured, but accesible to users with role ROLE_BOSS'() {
        when: 'try to visit announcement listing without login'
        go '/announcement/index'

        then: 'it is redirected to login page'
        at LoginPage

        when: 'signs in with a ROLE_BOSS user'
        login('sherlock', 'elementary')

        then: 'he gets access to the announcement listing page'
        at AnnouncementListingPage
    }

    void 'test /announcement/index is secured, but accesible to users with role ROLE_EMPLOYEE'() {
        when: 'try to visit announcement listing without login'
        go '/announcement/index'

        then: 'it is redirected to login page'
        at LoginPage

        when: 'signs in with a ROLE_EMPLOYEE user'
        login('watson', '221Bbakerstreet')

        then: 'he gets access to the announcement listing page'
        at AnnouncementListingPage
    }
}

4 Running the Application

We have removed the unit tests in this project. Our project contains only the integration and functional tests displayed in the previous code listings.

To run the tests:

./grailsw
grails> test-app
grails> open test-report

5 Appendix A

For versions of GORM older than 6.0.10 or Spring Security Core Plugin prior to 3.1.2, s2-quickstart handles password encoding directly with the injection of SpringSecurityService in the User class. Since Grails 3.2.8. service injection is disabled in domain classes. However, you can turn service injection on in domain classes:

grails-app/conf/application.yml
grails:
    gorm:
        # Whether to autowire entities.
        # Disabled by default for performance reasons.
        autowire: true

6 Do you need help with Grails?

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team