Categories
Moving to Grails

2FA – with Google Authenticator

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&hellip;"/></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