This blog post builds on the post “Security – Authentication”.
The grails part of this blog is an adaption of this guide.
I found some java code to use with Google authenticator and made it work.
The blog post will end with a runnable sample application.
Adding fields to store authenticator secret
We will need a toggle – isUsing2FA and the secret used to calculate the code. Then we will add a simple CRUD.
Edit the User domain definition – User.groovy
// Added fields
Boolean isUsing2FA = false
String secret
static constraints = {
// added constraint
secret nullable: true, blank: true
}
Then generate controller, service and views. From the terminal:
grails generate-all no.prpr.security.User
In order to be able to log out using the logout-controller – Append the following to application.groovy:
grails.plugin.springsecurity.logout.postOnly = false
Try to run the application and open the UserController – you will get an error message.
Now add @Secured annotation to the UserController:
...
import grails.plugin.springsecurity.annotation.Secured
@Secured('ROLE_ADMIN')
class UserController {
...
Now restart the application and open the UserController.
Adding 2FA code
In the package src/main/groovy/no.prpr.security add the file TwoFactorAuthenticationDetails.groovy
package no.prpr.security
import groovy.transform.Canonical
import groovy.transform.CompileStatic
import org.springframework.security.web.authentication.WebAuthenticationDetails
import javax.servlet.http.HttpServletRequest
/**
* TwoFactor authentication details class
*/
@Canonical
@CompileStatic
class TwoFactorAuthenticationDetails extends WebAuthenticationDetails {
Long code
TwoFactorAuthenticationDetails(HttpServletRequest request) {
super(request)
}
}
In the same package add the file TwoFactorAuthenticationDetailsSource.groovy:
package no.prpr.security
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.security.web.authentication.WebAuthenticationDetails
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import javax.servlet.http.HttpServletRequest
/**
* TwoFactor authentication details source class.
*
* Purpose: Request manipulation.
*/
@Slf4j
@CompileStatic
class TwoFactorAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
@Override
WebAuthenticationDetails buildDetails(HttpServletRequest context) {
log.debug("context: ${context}")
TwoFactorAuthenticationDetails details = new TwoFactorAuthenticationDetails(context)
log.debug("details: ${details}")
String sCode = obtainCode(context)
if ((sCode) && !sCode.empty) {
details.code = new Long(sCode)
}
log.debug("sCode: ${sCode}, details.code: ${details.code}")
details
}
/**
* Get the code from the request.
* @param request containing code
* @return two factor code
*/
private static String obtainCode(HttpServletRequest request) {
String result = request.getParameter('code')
log.debug("result: ${result}")
result
}
}
In the same package add the file AuthenticatorValidator.groovy
package no.prpr.security
import groovy.transform.CompileStatic
/**
* Authenticator validator class
*/
@CompileStatic
interface AuthenticatorValidator {
boolean isValidCodeForUser(Long code, String username)
}
In the same package add the file TwoFactorAuthenticationProvider.groovy
package no.prpr.security
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.beans.factory.annotation.Autowired
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
/**
* TwoFactor authentication provider class
*/
@Slf4j
@CompileStatic
class TwoFactorAuthenticationProvider extends DaoAuthenticationProvider {
@Autowired
AuthenticatorValidator authenticatorValidator
@SuppressWarnings(['Instanceof', 'LineLength'])
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
final String BAD_CREDENTIALS_CODE = 'AbstractUserDetailsAuthenticationProvider.badCredentials'
final String BAD_CREDENTIALS_DEFAULT = 'Bad credentials'
log.debug("userDetails: ${userDetails}, authentication: ${authentication}")
super.additionalAuthenticationChecks(userDetails, authentication)
Object details = authentication.details
log.debug("details.class: ${details.class}")
if ( !(details instanceof TwoFactorAuthenticationDetails) ) {
logger.debug('Authentication failed: authenticationToken principal is not a TwoFactorPrincipal')
throw new BadCredentialsException(messages.getMessage(BAD_CREDENTIALS_CODE, BAD_CREDENTIALS_DEFAULT))
}
TwoFactorAuthenticationDetails twoFactorAuthenticationDetails = details as TwoFactorAuthenticationDetails
log.debug("twoFactorAutthenticationDetails: ${twoFactorAuthenticationDetails}")
log.debug("twoFactorAutthenticationDetails.code: ${twoFactorAuthenticationDetails.code}")
log.debug("authentication.name: ${authentication.name}")
log.debug("authenticatorValidator: ${authenticatorValidator}")
if ( !authenticatorValidator.isValidCodeForUser(twoFactorAuthenticationDetails.code, authentication.name) ) {
logger.debug('Authentication failed: code not valid')
throw new BadCredentialsException(messages.getMessage(BAD_CREDENTIALS_CODE, BAD_CREDENTIALS_DEFAULT))
}
}
}
As you can see from logging in this file there were some issues on the way.
Now navigate to grails-app/services/no.prpr.security and edit file UserService.groovy. Replace the contents with:
package no.prpr.security
import grails.gorm.services.Service
import org.apache.commons.codec.binary.Base32
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.util.concurrent.TimeUnit
interface IUserService {
User get(Serializable id)
User findByUsername(String username)
List<User> list(Map args)
Long count()
void delete(Serializable id)
User save(User user)
}
@Service(User)
abstract class UserService implements IUserService {
/**
* resets secret of user
*
* @param id of user
* @param updateExisting create or update
* @return message
*/
String resetSecret(Serializable id, boolean updateExisting = true) {
log.debug("id: ${id} updateExisting: ${updateExisting}")
String result = ''
if (id) {
User user = get(id)
if (user) {
log.debug("user ${user} found")
boolean created = (!user.secret) || (user.secret.empty)
if ((user.secret) && (!user.secret?.empty) && (!updateExisting)) {
result = 'Secret exist, should not be updated'
} else {
// har funnet bruker uten secret
user.secret = generateSecret()
user.isUsing2FA = true
save(user)
result = "Secret for user ${user.username} ${created ? 'created' : 'updated'}"
}
} else {
result = CtlConst.NOT_FOUND
}
} else {
result = CtlConst.NOT_FOUND
}
log.debug("result: ${result}")
result
}
/**
* remove secret from user with id
* @param id
* @return
*/
String removeSecret(Serializable id) {
String result
User user = get(id)
if (user) {
user.secret = ''
user.isUsing2FA = false
save(user)
result = 'Secret removed'
} else {
result = 'User not found'
}
result
}
/**
* Generates a secret to use with 2fa
*
* @return secret
*/
String generateSecret() {
// log.debug('')
// Allocating the buffer
byte[] buffer = new byte[10 * 8 * 5]
//println buffer.length
new SecureRandom().nextBytes(buffer)
// Getting the key and converting it to Base32
Base32 codec = new Base32()
byte[] secretKey = Arrays.copyOf(buffer, 10)
byte[] bEncodedKey = codec.encode(secretKey)
String encodedKey = new String(bEncodedKey)
log.debug(encodedKey)
encodedKey
}
/**
* generates url to qr code for user/secret/hostname/issuer
*
* @param user username
* @param secret secret to generate
* @param hostname hostname of server/service
* @param issuer software/service issuer
* @return url
*/
@SuppressWarnings(['LineLength'])
String generateQRCodeURL (String user, String secret, String hostname, String issuer) {
String format = 'https://chart.googleapis.com/chart?chs=200x200&chld=H|0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s%%26issuer%%3D%s'
String.format(format, user, hostname, secret, issuer)
}
/**
* Checks secret / code
*
* @param secret to check
* @param code from user
* @return true if good
*/
boolean checkCode(String secret, long code) {
log.debug("secret: ${secret}, code: ${code}")
boolean result = false
Long timeUnits = new Date().time / TimeUnit.SECONDS.toMillis(30) as Long
Base32 codec = new Base32()
byte[] decodedKey = codec.decode(secret)
int window = 3
for (int i = -window; ((i <= window) && (!result)); ++i) {
long hash = verifyCode(decodedKey, timeUnits + i)
result = (hash == code)
}
log.debug("end: ${result}")
result
}
/**
* create secret for authUser with id
*
* @param id authUser.id
* @param updateExisting update / create
* @return message
*/
@SuppressWarnings('FactoryMethodName')
String createSecret(Serializable id, boolean updateExisting) {
log.debug("id: ${id} updateExisting: ${updateExisting}")
String result = ''
if (id) {
User user = userService.get(id)
if (user) {
log.debug("user ${user} funnet")
boolean created = (!user.secret) || (user.secret.empty)
if ((user.secret) && (!user.secret?.empty) && (!updateExisting)) {
result = 'Secret exist, should not be updated'
} else {
// har funnet bruker uten secret
user.secret = generateSecret()
userService.save(user)
result = "Secret for user ${user.username} ${created ? 'created' : 'updated'}"
}
} else {
result = 'Not found'
}
} else {
result = 'Not found'
}
log.debug("result: ${result}")
result
}
//
// private methods
//
/**
* part of checkCode
*
* @param key to check
* @param t ?
* @return hash
*/
private long verifyCode(byte[] key, long t) {
byte[] data = new byte[8]
long value = t
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value
}
SecretKeySpec signKey = new SecretKeySpec(key, 'HmacSHA1')
Mac mac = Mac.getInstance('HmacSHA1')
mac.init(signKey)
byte[] hash = mac.doFinal(data)
int offset = hash[20 - 1] & 0xF
long truncatedHash = 0
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8
truncatedHash |= (hash[offset + i] & 0xFF)
}
truncatedHash &= 0x7FFFFFFF
truncatedHash %= 1000000
(int) truncatedHash
}
}
In the same package add AuthenticatorValidatorService.groovy
package no.prpr.security
import grails.core.GrailsApplication
import grails.gorm.transactions.Transactional
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.springframework.beans.factory.annotation.Autowired
/**
* Authenticator validator service class
*
* In entreprenør / quickreg set tenantNeeded = true
*/
@Transactional
@CompileStatic
@SuppressWarnings('GrailsStatelessService')
class AuthenticatorValidatorService implements AuthenticatorValidator {
@Autowired
UserService userService
@Autowired
GrailsApplication grailsApplication
Boolean tenantNeeded //= { grailsApplication.config.prpr.tenantNeeded }
/**
* @param code to validate
* @param username entering code
* @return true if:
* a) user found, user.isUsing2FA and code is good
* b) user is found, !user.isUsing2FA
*/
@Transactional(readOnly = true)
@Override
@SuppressWarnings(['LineLength'])
boolean isValidCodeForUser(Long code, String username) {
log.debug("code: ${code}, username: ${username}")
boolean result = false
User user = userService.findByUsername(username)
if (user != null) {
//
// user is found
//
if (user.isUsing2FA) {
// isUsing2FA
if (code != null) {
// has code
result = userService.checkCode(user.secret, code)
}
} else {
result = true
}
}
log.debug("code: ${code}, username: ${username}, isUsing2FA: ${user?.isUsing2FA}, result: ${result}")
result
}
/**
* Use this method to initialize tenantNeeded
*/
@CompileDynamic
void initTenantNeeded() {
tenantNeeded = grailsApplication.config.prpr.tenantNeeded
}
}
Open grails-app/conf/spring/resources.groovy and replace its contents with:
import no.prpr.security.AuthenticatorValidatorService
import no.prpr.security.TwoFactorAuthenticationDetailsSource
import no.prpr.security.TwoFactorAuthenticationProvider
import no.prpr.security.UserPasswordEncoderListener
// Place your Spring DSL code here
beans = {
userPasswordEncoderListener(UserPasswordEncoderListener)
authenticationDetailsSource(TwoFactorAuthenticationDetailsSource)
authenticatorValidator(AuthenticatorValidatorService)
twoFactorAuthenticationProvider(TwoFactorAuthenticationProvider) {
userDetailsService = ref('userDetailsService')
passwordEncoder = ref('passwordEncoder')
userCache = ref('userCache')
preAuthenticationChecks = ref('preAuthenticationChecks')
postAuthenticationChecks = ref('postAuthenticationChecks')
authoritiesMapper = ref('authoritiesMapper')
hideUserNotFoundExceptions = true
authenticatorValidator = ref('authenticatorValidatorService')
}
}
Open grails-app/controllers/no.prpr.security/UserController and add the following methods
def resetsecret(Long id) {
request.withFormat {
'*' {
flash.message = userService.resetSecret(id)
redirect userService.get(id)
}
}
}
def removeSecret(Long id) {
log.debug("userTen.id: ${id}")
if (id == null) {
notFound()
return
}
request.withFormat {
'*' {
flash.message = userService.removeSecret(id)
redirect userService.get(id)
}
}
}
@CompileDynamic
def showqr(Long id) {
log.debug("user.id: ${id}")
Long code = 0
Long internalId = id
if (params) {
log.debug("params: ${params}")
internalId ?: params.id
code = params.getLong('code', 0)
log.debug("code: ${code}")
}
User user = userService.get(internalId)
String key = user.secret
if (key) {
String url = userService.generateQRCodeURL(user.username, key, 'johnny.prpr.no', 'johnny')
if (code != 0) {
if (userService.checkCode(key, code)) {
withFormat {
html {
flash.message = message(code: 'authenticator.register.message.codegood')
render(view: 'showqr', id: internalId, model: [id: internalId, key: key, url: url, status: OK])
}
}
} else {
log.error('code invalid')
withFormat {
html {
flash.message = message(code: 'authenticator.register.message.error', default: 'code invalid')
render(view: 'showqr', id: internalId,
model: [id: internalId, key: key, url: url, status: NOT_FOUND])
}
}
}
} else {
log.debug("code = ${code}")
render(view: 'showqr', id: internalId, model: [id: internalId, key: key, url: url])
}
} else {
notFound()
}
}
Open the show.gsp in grails-app/views/user folder and add the following links / buttons inside <fieldset class=”buttons”>
<fieldset class="buttons">
...
<g:link action="resetsecret"
resource="${this.user}"
onclick="return confirm('${message(code: 'default.confirm.message')}');">
<g:message
code="default.button.resetsecret.label"
default="Reset secret" />
</g:link>
<g:if test="${this.user.secret}">
<g:link action="removeSecret"
resource="${this.user}"
onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" >
<g:message
code="default.button.removesecret.label"
default="Remove secret" />
</g:link>
<g:link action="showqr"
resource="${this.user}">
<g:message
code="default.button.showqr.label"
default="Show QR-code"/>
</g:link>
</g:if>
...
</fieldset>
Add the showqr.gsp to the grails-app/views/user folder
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'authenticator.label', default: 'Authenticator')}" />
<title><g:message code="default.show.label" args="[entityName]" /></title>
</head>
<body>
<a href="#show-authUser" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div id="show-user" class="content scaffold-show" role="main">
<%-- <h1><g:message code="default.show.label" args="[entityName]" /></h1> --%>
<h1><g:message code="authenticator.register.message.instruction" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<table>
<tr>
<td>
<img src='${url}'/>
</td>
</tr>
<tr>
<td>
<g:message code="authenticator.register.message.sample" args="[entityName]" />
</td>
</tr>
<tr>
<td>
<form action="<g:createLink action="showqr" controller="${this.user}" />" method="post" id="registerCodeForm">
<g:hiddenField name="id" value="${id}" />
<input name="code"/>
<fieldset class="buttons">
<input class="prpr-icon-refresh" type="submit" value="${message(code: 'default.button.submit.label', default: 'Submit code')}" />
</fieldset>
</form>
</td>
</tr>
</table>
</div>
<script>
(function() {
document.forms['registerCodeForm'].elements['code'].focus();
})();
</script>
</body>
</html>
Append to grails-app/i18n/messages.properites
authenticator.register.message.instruction=First Register with Google Authenticator by using this QR Code:
authenticator.register.message.sample=Now enter in a sample code
authenticator.register.message.error=Code doesn't match
authenticator.register.message.codegood=Code matches!
We need a new login-page. Create the folder grails-app/views/login. Add the file 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: 440px;
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: 205px;
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: -205px;
width: 210px;
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>
<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="code"><g:message code="springSecurity.login.code.label" default="2FA code"/></label>
<input type="text" class="text_" name="code" id="code"/>
</p>
<p> <%-- id="remember_me_holder"> --%>
<label for="remember_me"><g:message code='springSecurity.login.remember.me.label'/></label>
<input type="checkbox" class="chk" name="${rememberMeParameter ?: 'remember-me'}" id="remember_me" <g:if test='${hasCookie}'>checked="checked"</g:if>/>
</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>
And finally reopen the application.groovy and append
grails.plugin.springsecurity.providerNames = [
'twoFactorAuthenticationProvider',
'anonymousAuthenticationProvider',
'rememberMeAuthenticationProvider']
Start the application:
Open the no.prpr.security.UserController. You will find the new login page with 2FA code:
Open the admin user and “Reset secret”.
There’s a toggle – isUsing2FA to enable / disable 2FA login for each account.
Initialize Google Authenticator and test with “Show QR-code”.
Try to log out with 2FA enabled. You will not be able to log in again without correct code.
The source code is here for simplicity