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:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/grails-spring-security-core-plugin-custom-authentication.git
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 theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-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
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:
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:
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
}
}
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
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:
package demo
import grails.gorm.services.Service
@Service(Role)
interface RoleService {
Role save(String authority)
Role findByAuthority(String authority)
}
package demo
import grails.gorm.services.Service
@Service(User)
interface UserService {
User findByUsername(String username)
User save(User user)
}
package demo
import grails.gorm.services.Service
@Service(UserRole)
interface UserRoleService {
UserRole save(User user, Role role)
}
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.
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
---
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
<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.
package demo
import groovy.transform.CompileStatic
@CompileStatic
interface CoordinateValidator {
boolean isValidValueForPositionAndUserName(String value, String position, String username)
}
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
coordinateValidator(CoordinateValidatorService)
We extend the DaoAuthenticationProvider
to add our extra check.
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.
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.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.
authenticationDetailsSource(TwoFactorAuthenticationDetailsSource)
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')
}
}
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.
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')
}
}
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