Show Navigation

Grails Spring Security Core Plugin Custom Authentication

Shows how to create a custom authentication with Spring Security Core Plugin

Authors: Sergio del Amo

Grails Version: 4.0.1

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 write a custom authentication mechanism. Spring Security Core Plugin allows for a significant degree of customization which we are going to explore next.

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.8 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-spring-security-core-plugin-custom-authentication/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-spring-security-core-plugin-custom-authentication/complete

3 Writing the Application

When we talk about two-factor authentication we often think about two things:

  • Something the user knows (e.g. username/password)

  • Something the user has.

For the latter, in Spain, banks often issue a Coordinate Card

caja espagna coordenadas

Users need to enter username, password and a coordinate to login into their bank.

We are going to customize Spring Security Core Plugin to achieve such a login in a Grails 3 application.

First, We need to add Spring Security Core Plugin as a dependency:

/build.gradle
compile 'org.grails.plugins:spring-security-core:4.0.3'

3.1 Domain Classes

Use s2-quickstart to generate the default Spring Security Core domain classes:

grails s2-quickstart demo User Role

s2-quickstart script generates three domain classes; User, Role and UserRole. It is recommended to move the password encoding logic outside of the domain class.

Service injection in GORM entities is disabled by default since Grails 3.2.8.

Each user is going to have a coordinate card. Thus we have modified the User domain class slightly:

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

import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

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

        private static final long serialVersionUID = 1

        transient springSecurityService

        String username
        String password
        boolean enabled = true
        boolean accountExpired
        boolean accountLocked
        boolean passwordExpired
        static hasMany = [coordinates: SecurityCoordinate]

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

        def beforeInsert() {
                encodePassword()
        }

        def beforeUpdate() {
                if (isDirty('password')) {
                        encodePassword()
                }
        }

        protected void encodePassword() {
                password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
        }

        static transients = ['springSecurityService']

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

        static mapping = {
                password column: '`password`'
                autowire true
        }
}
/grails-app/domain/demo/SecurityCoordinate.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class SecurityCoordinate {
    String position
    String value
    static belongsTo = [user: User]
}

3.2 Secured Controller

This controller is restricted to users with role ROLE_CLIENT

grails-app/controllers/demo/BankController.groovy
package demo

import grails.plugin.springsecurity.annotation.Secured

class BankController {

    @Secured(['ROLE_CLIENT'])
    def index() {
        render 'Welcome to your bank'
    }
}

3.3 Seed Data

We are going to populate our database with some seed data:

/grails-app/services/demo/RoleService.groovy
package demo

import grails.gorm.services.Service

@Service(Role)
interface RoleService {
    Role save(String authority)
    Role findByAuthority(String authority)
}
/grails-app/services/demo/UserService.groovy
package demo

import grails.gorm.services.Service

@Service(User)
interface UserService {

    User findByUsername(String username)

    User save(User user)
}
/grails-app/services/demo/UserRoleService.groovy
package demo

import grails.gorm.services.Service

@Service(UserRole)
interface UserRoleService {

    UserRole save(User user, Role role)
}
/grails-app/init/demo/BootStrap.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BootStrap {

    RoleService roleService
    UserService userService
    UserRoleService userRoleService
    static Map<String, String> BANKCARD =
            ['A1': '10', 'A2': '84', 'A3': '93', 'A4': '12', 'A5': '92',
             'A6': '58', 'A7': '38', 'A8': '28', 'A9': '36', 'A10': '02',
             'B1': '99', 'B2': '29', 'B3': '10', 'B4': '23', 'B5': '33',
             'B6': '47', 'B7': '58', 'B8': '39', 'B9': '34', 'B10': '18',
             'C1': '28', 'C2': '05', 'C3': '29', 'C4': '03', 'C5': '94',
             'C6': '14', 'C7': '41', 'C8': '33', 'C9': '11', 'C10': '39',
             'D1': '01', 'D2': '49', 'D3': '39', 'D4': '79', 'D5': '53',
             'D6': '38', 'D7': '17', 'D8': '88', 'D9': '70', 'D10': '12'
            ]

    def init = { servletContext ->
        List<String> authorities = ['ROLE_CLIENT']
        authorities.each { authority ->
            if ( !roleService.findByAuthority(authority) ) {
                roleService.save(authority)
            }
        }
        if ( !userService.findByUsername('sherlock') ) {
            User u = new User(username: 'sherlock', password: 'elementary')
            BANKCARD.each { k, v ->
                u.addToCoordinates(new SecurityCoordinate(position: k, value: v, user: u))
            }
            u = userService.save(u)
            userRoleService.save(u, roleService.findByAuthority('ROLE_CLIENT'))
        }
    }

    def destroy = {
    }
}

3.4 Custom Login Form

We override, both LoginController and auth.gsp, to display a random coordinate field each time the user is directed to the login form.

/grails-app/controllers/demo/LoginController.groovy
package demo

import grails.config.Config
import grails.core.support.GrailsConfigurationAware

class LoginController extends grails.plugin.springsecurity.LoginController implements GrailsConfigurationAware {

    List<String> coordinatePositions

    def auth() {

        ConfigObject conf = getConf()

        if (springSecurityService.isLoggedIn()) {
            redirect uri: conf.successHandler.defaultTargetUrl
            return
        }

        Collections.shuffle(coordinatePositions)
        String position = coordinatePositions.first()

        String postUrl = request.contextPath + conf.apf.filterProcessesUrl
        render view: 'auth', model: [postUrl: postUrl,
                                     rememberMeParameter: conf.rememberMe.parameter,
                                     usernameParameter: conf.apf.usernameParameter,
                                     passwordParameter: conf.apf.passwordParameter,
                                     gspLayout: conf.gsp.layoutAuth,
                                     position: position]
    }

    @Override
    void setConfiguration(Config co) {
        coordinatePositions = co.getProperty('security.coordinate.positions', List, []) as List<String>

    }
}

We have a list of valid positions as a configuration list in application.yml

grails-app/conf/application.yml
---
security:
    coordinate:
        positions:
            - A1
            - A2
            - A3
            - A4
            - A5
            - A6
            - A7
            - A8
            - A9
            - A10
            - B1
            - B2
            - B3
            - B4
            - B5
            - A6
            - A7
            - B8
            - B9
            - B10
            - C1
            - C2
            - C3
            - C4
            - C5
            - C6
            - C7
            - C8
            - C9
            - C10
            - D1
            - D2
            - D3
            - D4
            - D5
            - D6
            - D7
            - D8
            - D9
            - D10

The overriden GSP file displays the coordinate position input field

login
/grails-app/views/login/auth.gsp
<html>
<head>
    <meta name="layout" content="${gspLayout ?: 'main'}"/>
    <title><g:message code='springSecurity.login.title'/></title>
    <style type="text/css" media="screen">
    #login {
        margin: 15px 0px;
        padding: 0px;
        text-align: center;
    }

    #login .inner {
        width: 340px;
        padding-bottom: 6px;
        margin: 60px auto;
        text-align: left;
        border: 1px solid #aab;
        background-color: #f0f0fa;
        -moz-box-shadow: 2px 2px 2px #eee;
        -webkit-box-shadow: 2px 2px 2px #eee;
        -khtml-box-shadow: 2px 2px 2px #eee;
        box-shadow: 2px 2px 2px #eee;
    }

    #login .inner .fheader {
        padding: 18px 26px 14px 26px;
        background-color: #f7f7ff;
        margin: 0px 0 14px 0;
        color: #2e3741;
        font-size: 18px;
        font-weight: bold;
    }

    #login .inner .cssform p {
        clear: left;
        margin: 0;
        padding: 4px 0 3px 0;
        padding-left: 105px;
        margin-bottom: 20px;
        height: 1%;
    }

    #login .inner .cssform input[type="text"] {
        width: 120px;
    }

    #login .inner .cssform label {
        font-weight: bold;
        float: left;
        text-align: right;
        margin-left: -105px;
        width: 110px;
        padding-top: 3px;
        padding-right: 10px;
    }

    #login #remember_me_holder {
        padding-left: 120px;
    }

    #login #submit {
        margin-left: 15px;
    }

    #login #remember_me_holder label {
        float: none;
        margin-left: 0;
        text-align: left;
        width: 200px
    }

    #login .inner .login_message {
        padding: 6px 25px 20px 25px;
        color: #c33;
    }

    #login .inner .text_ {
        width: 120px;
    }

    #login .inner .chk {
        height: 12px;
    }
    </style>
</head>

<body>
<div id="login">
    <div class="inner">
        <div class="fheader"><g:message code='springSecurity.login.header'/></div>

        <g:if test='${flash.message}'>
            <div class="login_message">${flash.message}</div>
        </g:if>

        <form action="${postUrl ?: '/login/authenticate'}" method="POST" id="loginForm" class="cssform" autocomplete="off">
            <p>
                <label for="username"><g:message code='springSecurity.login.username.label'/>:</label>
                <input type="text" class="text_" name="${usernameParameter ?: 'username'}" id="username"/>
            </p>

            <p>
                <label for="password"><g:message code='springSecurity.login.password.label'/>:</label>
                <input type="password" class="text_" name="${passwordParameter ?: 'password'}" id="password"/>
            </p>

            <p>
                <label for="coordinateValue">${position}</label>
                <input type="hidden" name="coordinatePosition" id="coordinatePosition" value="${position}"/>
                <input type="text" class="text_" name="coordinateValue" id="coordinateValue"/>
            </p>

            <p id="remember_me_holder">
                <input type="checkbox" class="chk" name="${rememberMeParameter ?: 'remember-me'}" id="remember_me" <g:if test='${hasCookie}'>checked="checked"</g:if>/>
                <label for="remember_me"><g:message code='springSecurity.login.remember.me.label'/></label>
            </p>

            <p>
                <input type="submit" id="submit" value="${message(code: 'springSecurity.login.button')}"/>
            </p>
        </form>
    </div>
</div>
<script>
    (function() {
        document.forms['loginForm'].elements['${usernameParameter ?: 'username'}'].focus();
    })();
</script>
</body>
</html>

3.5 Authentication Provider

Create a Custom AuthenticationProvider implementation. That it is a class which implements:

org.springframework.security.authentication.AuthenticationProvider

You will normally:

  • Either extend one that it is similar. For example, DaoAuthenticationProvider which comes with the plugin.

org.springframework.security.authentication.dao.DaoAuthenticationProvider

  • Or directly implement the AuthenticationProvider interface

In this guide, we choose the first option.

However, Let’s start with the artifact we are going to use to validate the coordinate supplied by the user. In this guide, we use a Grails Service.

/src/main/groovy/demo/CoordinateValidator.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
interface CoordinateValidator {

    boolean isValidValueForPositionAndUserName(String value, String position, String username)
}
/grails-app/services/demo/CoordinateValidatorService.groovy
package demo

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@Transactional
@CompileStatic
class CoordinateValidatorService implements CoordinateValidator {

    @ReadOnly
    @Override
    boolean isValidValueForPositionAndUserName(String v, String p, String name) {
        SecurityCoordinate.where {
            position == p && value == v && user.username == name
        }.count() as boolean
    }
}

We are going to inject this service as a bean in our custom authentication provider. In order to do that, we register the bean first

/grails-app/conf/spring/resources.groovy
coordinateValidator(CoordinateValidatorService)

We extend the DaoAuthenticationProvider to add our extra check.

/src/main/groovy/demo/TwoFactorAuthenticationProvider.groovy
package demo

import groovy.transform.CompileStatic
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.userdetails.UserDetails

@CompileStatic
class TwoFactorAuthenticationProvider extends DaoAuthenticationProvider {

    CoordinateValidator coordinateValidator

    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {

        super.additionalAuthenticationChecks(userDetails, authentication)

        Object details = authentication.details

        if ( !(details instanceof TwoFactorAuthenticationDetails) ) {
            logger.debug("Authentication failed: authenticationToken principal is not a TwoFactorPrincipal");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
        def twoFactorAuthenticationDetails = details as TwoFactorAuthenticationDetails


        if ( !coordinateValidator.isValidValueForPositionAndUserName(twoFactorAuthenticationDetails.coordinateValue, twoFactorAuthenticationDetails.coordinatePosition, authentication.name) ) {
            logger.debug("Authentication failed: coordiante note valid");
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

}

We need to register our custom AuthenticationProvider as a bean.

/grails-app/conf/spring/resources.groovy
twoFactorAuthenticationProvider(TwoFactorAuthenticationProvider) {
    coordinateValidator = ref('coordinateValidator')
    userDetailsService = ref('userDetailsService')
    passwordEncoder = ref('passwordEncoder')
    userCache = ref('userCache')
    preAuthenticationChecks = ref('preAuthenticationChecks')
    postAuthenticationChecks = ref('postAuthenticationChecks')
    authoritiesMapper = ref('authoritiesMapper')
    hideUserNotFoundExceptions = true
}

Moreover, we want to use our TwoFactorAuthenticationProvider instead of DaoAuthenticationProvider. In order to do that, we define the providers which should be use to authenticate:

/grails-app/conf/application.groovy
grails.plugin.springsecurity.providerNames = [
                'twoFactorAuthenticationProvider',
                'anonymousAuthenticationProvider',
                'rememberMeAuthenticationProvider']

3.6 Authentication

In order to create a custom authentication, we often need a custom Authentication.

org.springframework.security.core.Authentication

Authentication represents the token for an authentication request or an authenticated principal once the request has been processed by the method:

AuthenticationManager.authenticate(Authentication)

This generally means:

  • Either extend an existing implementation.

org.springframework.security.authentication.UsernamePasswordAuthenticationToken

  • Or directly implement the interface Authentication

What we are trying to achieve is an extension of the common username/password functionality offered already by the plugin. We don’t need a custom authentication object. Instead, we are going to use the UsernamePasswordAuthenticationToken object and place in the details property our custom information; the coordinate.

3.7 Filter

If you are using a custom Authentication, you may need to create a filter.

That means coding a custom java.servlet.Filter implementation.

You have several options:

  • Extend GenericFilterBean

org.springframework.web.filter.GenericFilterBean

  • Extend a similar filter e.g. UsernamePasswordAuthenticationFilter

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

  • Directly implement the interface

For this guide, we don’t need to create a custom filter. Instead, we are using the UsernamePasswordAuthenticationFilter and overriding the authenticationDetailsSource bean used by that filter.

/grails-app/conf/spring/resources.groovy
authenticationDetailsSource(TwoFactorAuthenticationDetailsSource)
/src/main/groovy/demo/TwoFactorAuthenticationDetailsSource.groovy
package demo

import groovy.transform.CompileStatic
import org.springframework.security.web.authentication.WebAuthenticationDetails
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource

import javax.servlet.http.HttpServletRequest

@CompileStatic
class TwoFactorAuthenticationDetailsSource extends WebAuthenticationDetailsSource {

    @Override
    WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        TwoFactorAuthenticationDetails details = new TwoFactorAuthenticationDetails(context)

        String position = obtainCoordinatePosition(context)
        details.coordinatePosition = position

        String value = obtainCoordinateValue(context)
        details.coordinateValue = value
        details
    }

    /**
     * Get the Coordinate Position from the request.
     * @param request
     * @return
     */
    private static String obtainCoordinatePosition(HttpServletRequest request) {
        return request.getParameter('coordinatePosition')
    }

    /**
     * Get the Coordinate Value from the request.
     * @param request
     * @return
     */
    private static String obtainCoordinateValue(HttpServletRequest request) {
        return request.getParameter('coordinateValue')
    }
}
/src/main/groovy/demo/TwoFactorAuthenticationDetails.groovy
package demo

import groovy.transform.Canonical
import groovy.transform.CompileStatic
import org.springframework.security.web.authentication.WebAuthenticationDetails

import javax.servlet.http.HttpServletRequest

@Canonical
@CompileStatic
class TwoFactorAuthenticationDetails extends WebAuthenticationDetails {
    String coordinatePosition
    String coordinateValue

    TwoFactorAuthenticationDetails(HttpServletRequest request) {
        super(request)
    }
}

3.8 Functional Test

Grails integrate seamlessly with Geb. Thus it is easy to develop a functional test for our custom login form.

/src/integration-test/groovy/demo/BankControllerSpec.groovy
package demo

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

@Integration
class BankControllerSpec extends GebSpec {

    def 'test bank controller is secured'() {
        when:
        baseUrl = "http://localhost:${serverPort}/"
        go 'bank'

        then:
        at LoginPage

        when:
        LoginPage loginPage = browser.page(LoginPage)
        loginPage.login('sherlock', 'elementary', BootStrap.BANKCARD[loginPage.position()])

        then:
        driver.pageSource.contains('Welcome to your bank')
    }
}
/src/integration-test/groovy/demo/LoginPage.groovy
package demo

import geb.Page

class LoginPage extends Page {

    static url = 'login/auth'

    static at = { title == 'Login' }

    static content = {
        usernameField { $('#username', 0) }
        passwordField { $('#password', 0) }
        positionField { $('#coordinatePosition', 0)}
        valueField { $('#coordinateValue', 0) }
        submitField { $('#submit', 0) }
    }

    String position() {
        positionField.getAttribute('value')
    }

    void login(String username, String password, String value) {
        usernameField << username
        passwordField << password
        valueField << value
        submitField.click()
    }
}

4 Running the Application

To run the tests:

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

5 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