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.8

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/example/grails/Announcement.groovy
| Created src/test/groovy/example/grails/AnnouncementSpec.groovy
grails-app/domain/example/grails/Announcement.groovy
package example.grails

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/example/grails/AnnouncementController.groovy
| Rendered template Spec.groovy to destination src/test/groovy/example/grails/AnnouncementControllerSpec.groovy
| Scaffolding completed for grails-app/domain/example/grails/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/example/grails/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/example/grails/ApiAnnouncementController.groovy
| Created src/test/groovy/example/grails/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/example/grails/UrlMappings.groovy
'/api/announcements'(controller: 'apiAnnouncement')

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

grails-app/controllers/example/grails/ApiAnnouncementController.groovy
package example.grails

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.RC1'
    compile 'org.grails.plugins:spring-security-core:3.2.3'

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

./grailsw s2-quickstart example.grails User SecurityRole

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

grails-app/domain/example/grails/User.groovy
package example.grails

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/example/grails/SecurityRole.groovy
package example.grails

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/example/grails/UserSecurityRole.groovy
package example.grails

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/example/grails/UserPasswordEncoderListener.groovy
package example.grails

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 example.grails.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 {
            rest {
                token {
                    storage {
                        jwt {
                            secret = 'pleaseChangeThisSecretForANewOne'
                        }
                    }
                }
            }
            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 = 'example.grails.User' (4)
                authorityJoinClassName = 'example.grails.UserSecurityRole' (4)
            }
            authority {
                className = 'example.grails.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/services/example/grails/UserService.groovy
package example.grails

import grails.gorm.services.Service

@Service(User)
interface UserService {

    User save(String username, String password)

    User findByUsername(String username)
}
grails-app/services/example/grails/SecurityRoleService.groovy
package example.grails

import grails.gorm.services.Service

@Service(SecurityRole)
interface SecurityRoleService {

    SecurityRole save(String authority)

    SecurityRole findByAuthority(String authority)
}
grails-app/services/example/grails/UserSecurityRoleService.groovy
package example.grails

import grails.gorm.services.Service

@Service(UserSecurityRole)
interface UserSecurityRoleService {

    UserSecurityRole save(User user, SecurityRole securityRole)
}
grails-app/init/example/grails/BootStrap.groovy
package example.grails

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BootStrap {

    AnnouncementService announcementService

    UserService userService

    SecurityRoleService securityRoleService

    UserSecurityRoleService userSecurityRoleService

    def init = { servletContext ->

        List<String> authorities = ['ROLE_BOSS', 'ROLE_EMPLOYEE']
        authorities.each { String authority ->
            if ( !securityRoleService.findByAuthority(authority) ) {
                securityRoleService.save(authority)
            }
        }

        if ( !userService.findByUsername('sherlock') ) {
            User u = userService.save('sherlock', 'elementary')
            userSecurityRoleService.save(u, securityRoleService.findByAuthority('ROLE_BOSS'))
        }

        if ( !userService.findByUsername('watson') ) {
            User u = userService.save('watson', '221Bbakerstreet')
            userSecurityRoleService.save(u, securityRoleService.findByAuthority('ROLE_EMPLOYEE'))
        }

        announcementService.save('The Hound of the Baskervilles')
    }
    def destroy = {
    }
}

3.5 Test Rest Api

To test REST part of the application, we use Micronaut’s HttpClient. We need to add the dependency:

/gradle.properties
grailsVersion=3.3.8
gormVersion=6.1.10.RELEASE
gradleWrapperVersion=3.5
assetPipelineVersion=2.14.8
micronautVersion=1.2.6
gebVersion=2.3
seleniumVersion=3.14.0
/build.gradle
    testCompile "io.micronaut:micronaut-http-client:$micronautVersion"

Micronaut HTTP client makes easy to bind JSON Payloads to POGOs. Create several POGOs which we will use in the test:

/src/integration-test/groovy/example/grails/BearerToken.groovy
package example.grails

import com.fasterxml.jackson.annotation.JsonProperty

class BearerToken {
    @JsonProperty('access_token')
    String accessToken

    @JsonProperty('refresh_token')
    String refreshToken

    List<String> roles

    String username
}
/src/integration-test/groovy/example/grails/CustomError.groovy
package example.grails

import groovy.transform.CompileStatic

@CompileStatic
class CustomError {
    Integer status
    String error
    String message
    String path
}
/src/integration-test/groovy/example/grails/UserCredentials.groovy
package example.grails

import groovy.transform.CompileStatic

@CompileStatic
class UserCredentials {
    String username
    String password
}

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

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

import grails.testing.mixin.integration.Integration
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientException
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
import grails.testing.spock.OnceBefore

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

    @Shared
    @AutoCleanup
    HttpClient client

    @OnceBefore (1)
    void init() {
        client  = HttpClient.create(new URL("http://localhost:$serverPort")) (2)
    }

    def 'test /api/announcements url is secured'() {
        when:
        HttpRequest request = HttpRequest.GET('/api/announcements')
        client.toBlocking().exchange(request,  (3)
                Argument.of(List, AnnouncementView),
                Argument.of(CustomError))

        then:
        HttpClientException e = thrown(HttpClientException)
        e.response.status == HttpStatus.UNAUTHORIZED (4)

        when:
        Optional<CustomError> jsonError = e.response.getBody(CustomError)

        then:
        jsonError.isPresent()
        jsonError.get().status == 401
        jsonError.get().error == 'Unauthorized'
        jsonError.get().message == 'No message available'
        jsonError.get().path == '/api/announcements'
    }

    def "test a user with the role ROLE_BOSS is able to access /api/announcements url"() {
        when: 'login with the sherlock'
        UserCredentials credentials = new UserCredentials(username: 'sherlock', password: 'elementary')
        HttpRequest request = HttpRequest.POST('/api/login', credentials) (5)
        HttpResponse<BearerToken> resp = client.toBlocking().exchange(request, BearerToken)

        then:
        resp.status.code == 200
        resp.body().roles.find { it == 'ROLE_BOSS' }

        when:
        String accessToken = resp.body().accessToken

        then:
        accessToken

        when:
        HttpResponse<List> rsp = client.toBlocking().exchange(HttpRequest.GET('/api/announcements')
                .header('Authorization', "Bearer ${accessToken}"), Argument.of(List, AnnouncementView)) (6)

        then:
        rsp.status.code == 200 (7)
        rsp.body() != null
        ((List)rsp.body()).size() == 1
        ((List)rsp.body()).get(0) instanceof AnnouncementView
        ((AnnouncementView) ((List)rsp.body()).get(0)).message == 'The Hound of the Baskervilles'
    }

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

        UserCredentials creds = new UserCredentials(username: 'watson', password: '221Bbakerstreet')
        HttpRequest request = HttpRequest.POST('/api/login', creds)
        HttpResponse<BearerToken> resp = client.toBlocking().exchange(request, BearerToken)

        then:
        resp.status.code == 200
        !resp.body().roles.find { it == 'ROLE_BOSS' }
        resp.body().roles.find { it == 'ROLE_EMPLOYEE' }

        when:
        String accessToken = resp.body().accessToken

        then:
        accessToken

        when:
        resp = client.toBlocking().exchange(HttpRequest.GET('/api/announcements')
                .header('Authorization', "Bearer ${accessToken}"))

        then:
        def e = thrown(HttpClientException)
        e.response.status == HttpStatus.FORBIDDEN (8)
    }
}
1 The grails.testing.spock.OnceBefore annotation is a shorthand way of accomplishing the same behavior that would be accomplished by applying both the @RunOnce and @Before annotations to a fixture method.
2 serverPort property is automatically injected and it contains the random port where the app will be running for the functional test.
3 With Micronaut HTTP client you can bind POJOs to the response body easily.
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/example/grails/LoginPage.groovy
package example.grails

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/example/grails/AnnouncementListingPage.groovy
package example.grails

import geb.Page

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

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

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'
        LoginPage page = browser.page(LoginPage)
        page.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'
        LoginPage page = browser.page(LoginPage)
        page.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?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team