Show Navigation

Google OAuth2 with Grails 3 and Spring Security REST

Learn how to use Google OAuth2 with Grails 3 and Spring Security REST plugin

Authors: Ben Rhine

Grails Version: 3.3.2

1 Getting Started

In this guide we will show you how to add Google OAuth2 authentication to your app using the Spring Security Rest plugin.

We will be using the Spring Security Rest plugin

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

1.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-oauth-google/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-oauth-google/complete

If you want to start from scratch, create a new Grails 3 application using Grails Application Forge.

forgeDefault

2 OAuth2 Authentication

OAuth2 is an industry-standard authentication protocol used by many Fortune 500 companies to secure websites and applications. The mechanism by which it works allows for a third-party authorization server to issue access tokens by the account owner approving access. In our case we will be using Google so in more laymen’s term a Google user approves their account to issue access tokens back to the requesting application.

2.1 Setup and Configure Google OAuth2

To get Google OAuth2 up and running on your application will take a bit of work and configuration on the Google Developer Console.

googleDevelopersHome

Go ahead and sign in and then scroll to the bottom of the page to select Google API Console

apiConsoleSelect

Select Create Project

googleCreateProject

Give your Google project a name. In this case we name it Oauth-Test

projectName

Wait a moment while Google creates your new project

waitWhileProjectIsCreated

Select ENABLE APIS AND SERVICES

dashboardPostProjectCreate

At the search screen search for google+

apiSearch

Select Google+ API

searchGooglePlus

Click ENABLE and wait a moment while it turns on.

enableApi

Now back on the Dashboard click Create credentials

dashboardPostApi

Step 1: Make sure Google+ API is selected with the Web server options and User data then click `What credentials do I need?

addCredentials1

Step 2: Enter your chosen name or leave default, set your front end url and the plugin oauth/callback/google url then click Create client ID

addCredentials2

Step 3: Select your gmail and give the product a name so users will know what they are authenticating into. Click Continue

addCredentials4

Step 4: Download your credentials JSON and click Done

addCredentials5

That will bring you back to the credentials home page.

credentialsHome

This is all the setup that needs to be done in Google. Now we will take a look at setting up our applicatin and the credentials we downloaded to connect our app.

3 Setting up your app

With all our Google configuration in place its time get our app configured to use security and connect using OAuth2 over REST through Google.

The next diagramm describes the security solution we are going to implement.

diagramm

3.1 Add Security Dependencies

First thing we need to do is add the spring-security-core and spring-security-rest plugins to our build.gradle file.

build.gradle
compile 'org.grails.plugins:spring-security-core:3.2.1'
compile 'org.grails.plugins:spring-security-rest:2.0.0.RC1'

3.2 Custom Token Reader

We override the default token reader to read the JWT token from the cookies.

Implement a TokenReader

src/main/groovy/demo/JwtCookieTokenReader.groovy
package demo

import grails.plugin.springsecurity.rest.token.AccessToken
import grails.plugin.springsecurity.rest.token.reader.TokenReader
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest

@Slf4j
@CompileStatic
class JwtCookieTokenReader implements TokenReader {

    final static String DEFAULT_COOKIE_NAME = 'JWT'

    String cookieName = DEFAULT_COOKIE_NAME

    @Override
    AccessToken findToken(HttpServletRequest request) {

        log.debug "Looking for jwt token in a cookie named {}", cookieName
        String tokenValue = null
        Cookie cookie = request.getCookies()?.find { Cookie cookie -> cookie.name.equalsIgnoreCase(cookieName) }

        if ( cookie ) {
            tokenValue = cookie.value
        }

        log.debug "Token: ${tokenValue}"
        return tokenValue ? new AccessToken(tokenValue) : null

    }
}

Register it in grails-app/conf/spring/resources.groovy as tokenReader.

grails-app/conf/spring/resources.groovy
import demo.JwtCookieTokenReader
beans {
    tokenReader(JwtCookieTokenReader) {
        cookieName = 'jwt'
    }
...
..
.
}

3.3 Configure Security

With our dependencies added, we need to configure security.

Create a file application.groovy with the following content staticRules configuration.

grails-app/conf/application.groovy
grails.plugin.springsecurity.controllerAnnotations.staticRules = [
        [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']]
]

Add the next block to configure Grails Spring Security Rest Plugin:

grails-app/conf/application.groovy
grails {
        plugin {
                springsecurity {
                        rest {
                                token {
                                        validation {
                                                useBearerToken = false (1)
                                                enableAnonymousAccess = true (2)
                                        }
                                        storage {
                                                jwt {
                                                        secret = 'foobar123'*4 (3)
                                                }
                                        }
                                }
                                oauth {
                                        frontendCallbackUrl = { String tokenValue -> "http://localhost:8080/auth/success?token=${tokenValue}" } (4)
                                        google {
                                                client = org.pac4j.oauth.client.Google2Client (5)
                                                key = '${GOOGLE_KEY}' (6)
                                                secret = '${GOOGLE_SECRET}' (7)
                                                scope = org.pac4j.oauth.client.Google2Client.Google2Scope.EMAIL_AND_PROFILE (8)
                                                defaultRoles = [] (9)
                                        }
                                }
                        }
                        providerNames = ['anonymousAuthenticationProvider'] (10)
                }
        }
}
1 You must disable bearer token support to register your own tokenReader implementation.
2 Enable anonymous access to URL’s where the Grails Spring Security Rest plugin’s filters are applied
3 Required secret which is used to sign the JWT tokens.
4 Callback url, after selecting your google user this is callback url will be invoked with a JWT token which authenticates the user
5 Which pac4j client to use; in our case the Google client
6 Open your client_id.json you downloaded while setting up Google. You will supply client_id as the System Property GOOGLE_KEY when you start the app.
7 Open your client_id.json you downloaded while setting up Google. You will supply client_secret as the System Property GOOGLE_KEY when you start the app.
8 The scope can be from any value of the enum org.pac4j.oauth.client.Google2Client.Google2Scope
9 Specific roles that Google authentication is allowed to access
10 We are going to authenticate our users only against Google. Thus, we use only the anonymous authentication provider. Read more about Authentication Providers in Spring Security Core Plugin documentation.

To start the app you will need to supply client_id and client_secret as system properties. e.g.:

`./gradlew -DGOOGLE_KEY=XXXXXX -DGOOGLE_SECRET=XXXX bootRun

While the scope can be from any value of the enum org.pac4j.oauth.client.Google2Client.Google2Scope, if you use the default OauthUserDetailsService, you need to use EMAIL_AND_PROFILE. That is because the default implementation uses the profile ID as the username, and that is only returned by Google if EMAIL_AND_PROFILE scope is used.

We want our app stateless by default with some endpoints which allow anonymous access.

grails-app/conf/application.groovy
String ANONYMOUS_FILTERS = 'anonymousAuthenticationFilter,restTokenValidationFilter,restExceptionTranslationFilter,filterInvocationInterceptor' (1)
grails.plugin.springsecurity.filterChain.chainMap = [
                [pattern: '/dbconsole/**',      filters: 'none'],
                [pattern: '/assets/**',      filters: 'none'],
                [pattern: '/**/js/**',       filters: 'none'],
                [pattern: '/**/css/**',      filters: 'none'],
                [pattern: '/**/images/**',   filters: 'none'],
                [pattern: '/**/favicon.ico', filters: 'none'],
                [pattern: '/', filters: ANONYMOUS_FILTERS], (1)
                [pattern: '/book/show/*', filters: ANONYMOUS_FILTERS],  (1)
                [pattern: '/bookFavourite/index', filters: ANONYMOUS_FILTERS], (1)
                [pattern: '/auth/success', filters: ANONYMOUS_FILTERS], (1)
                [pattern: '/oauth/authenticate/google', filters: ANONYMOUS_FILTERS], (1)
                [pattern: '/oauth/callback/google', filters: ANONYMOUS_FILTERS], (1)
                [pattern: '/**', filters: 'JOINED_FILTERS,-anonymousAuthenticationFilter,-exceptionTranslationFilter,-authenticationProcessingFilter,-securityContextPersistenceFilter,-rememberMeAuthenticationFilter'],  (1)
]
1 stateless chain that allows anonymous access when no token is sent. If however a token is on the request, it will be validated.
2 /** is a stateless chain that doesn’t allow anonymous access. Thus, the token will always be required, and if missing, a Bad Request reponse will be sent back to the client.
We are not persisting User information in the database. You may have noticed we don’t have User, Role, UserRole domain classes in the project. Neither we setup configuration values such as userLookUp.userDomainClass etc.

Now we override the login/auth view. So that we no longer show a username/password form in that page.

grails-app/views/login/auth.gsp
<html>
<head>
    <meta name="layout" content="${gspLayout ?: 'main'}"/>
    <title><g:message code='springSecurity.login.title'/></title>
</head>

<body>
<div id="login">
    <div class="inner centered" >
        <div class="fheader"><g:message code='springSecurity.login.header'/></div>
        <g:if test='${flash.message}'>
            <div class="login_message">${flash.message}</div>
        </g:if>
    </div>
</div>
</body>
</html>

We don’t include a button to SignIn with Google in that page because we have included that button in root layout main.gsp file.

3.4 Logout Handlers

Spring Security allows to register custom Logout Handlers. Register a new logout handler to clear the JWT cookie. We reuse CookieClearingLogoutHandler which ships with Spring Security.

Modify grails-app/conf/spring/resources.groovy:

grails-app/conf/spring/resources.groovy
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
beans {
...
..
.
    cookieClearingLogoutHandler(CookieClearingLogoutHandler, ['jwt'])
}

Add the custom logout handler to Spring Security Core plugin logout.handlerNames.

grails-app/conf/application.groovy
grails.plugin.springsecurity.logout.handlerNames = ['rememberMeServices', 'securityContextLogoutHandler', 'cookieClearingLogoutHandler']

3.5 JWT Cookie

Create AuthController.groovy. When the user logs in successfully with Google, AuthController.success is invoked.

The path to AuthController.success is used in frontendCallbackUrl in application.groovy.
grails-app/controllers/demo/AuthController.groovy
package demo

import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import grails.plugin.springsecurity.annotation.Secured
import grails.plugin.springsecurity.rest.token.reader.TokenReader
import groovy.util.logging.Slf4j

import javax.servlet.http.Cookie

@Slf4j
class AuthController implements GrailsConfigurationAware {

    TokenReader tokenReader

    int jwtExpiration

    String grailsServerUrl

    static allowedMethods = [
            success: 'GET',
            logout: 'POST'
    ]

    @Secured('permitAll')
    def success(String token) {
        log.debug('token value {}', token)
        if (token) {
            Cookie cookie = jwtCookie(token)
            response.addCookie(cookie) (1)
        }
        [grailsServerUrl: grailsServerUrl]
    }

    protected Cookie jwtCookie(String tokenValue) {
        Cookie jwtCookie = new Cookie( cookieName(), tokenValue )
        jwtCookie.maxAge = jwtExpiration (5)
        jwtCookie.path = '/'
        jwtCookie.setHttpOnly(true) (3)
        if ( httpOnly() ) {
            jwtCookie.setSecure(true) (4)
        }
        jwtCookie
    }

    @Override
    void setConfiguration(Config co) {
        jwtExpiration = co.getProperty('grails.plugin.springsecurity.rest.token.storage.memcached.expiration', Integer, 3600) (5)
        grailsServerUrl = co.getProperty('grails.serverURL', String)
    }

    protected boolean httpOnly() {
        grailsServerUrl?.startsWith('https')
    }

    protected String cookieName() {
        if ( tokenReader instanceof JwtCookieTokenReader ) {
            return ((JwtCookieTokenReader) tokenReader).cookieName  (6)
        }
        return 'jwt'
    }
}
1 Responds a Cookie with the JWT token as value
2 Responding a cookie with the same name and maxAge equals 0 deletes the cookie. Thus, it logs out the user.
3 Prevents any Javascript executed in your site ( even your own javascript ) to do document.cookies and access the cookies
4 Cookie won’t leave if you do http:// instead of https://. You should use https in production.
5 Set the cookie expiration to match JWT expiration
6 Use the same cookie name, the custom tokenReader we previoulsy defined expects.
Due to the stateless nature of the security solution of this application. To log out a user involves the deletion the cookie containing his JWT token.

The GSP of success action performs simple redirect to the home page. We do the redirect in the client side to ensure the cookie is correctly set.

grails-app/views/auth/success.gsp
<html>
    <head>
        <meta http-equiv="refresh" content="0; url=${grailsServerUrl ?: 'http://localhost:8080'}/" />
        </head>
        <body><g:message code="redirecting" default="Redirecting..."/></body>
</html>

3.6 Enhanced Logging

If you would like to enable enhanced logging so you can see what is returned when calling Googles OAuth api, add the following to the end of your logback.groovy file

grails-app/conf/logback.groovy
logger("org.springframework.security", DEBUG, ['STDOUT'], false)
logger("grails.plugin.springsecurity", DEBUG, ['STDOUT'], false)
logger("org.pac4j", DEBUG, ['STDOUT'], false)

4 Building your app

We will quickly put together an app that lists books and allows us to favorite them. The favorite button will only be available once we have logged in using google.

4.1 Your Domain

For your domain we will need to domain objects Book and BookFavourite. Create as follows

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

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Book {
    String image
    String title
    String author
    String about
    String href
    static mapping = {
        image nullable: false
        title nullable: false
        author nullable: false
        about nullable: false
        href nullable: false
        about type: 'text'
    }
}
grails-app/domain/demo/BookFavourite.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class BookFavourite {
    Long bookId
    String username

    static constraints = {
        bookId nullable: false
        username nullable: false
    }
}

4.2 Your Data

At this point we will create our book data so we have something to work with moving forward. Go ahead and make your BootStrap.groovy match the following.

The required image resources are already added to the initial project for you.
grails-app/init/demo/BootStrap.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

    public final static List< Map<String, String> > GRAILS_BOOKS = [
            [
                    title : 'Grails 3 - Step by Step',
                    author: 'Cristian Olaru',
                    href: 'https://grailsthreebook.com/',
                    about : 'Learn how a complete greenfield application can be implemented quickly and efficiently with Grails 3 using profiles and plugins. Use the sample application that accompanies the book as an example.',
                    image: 'grails_3_step_by_step.png',
            ],
            [
                    title : 'Practical Grails 3',
                    author: ' Eric Helgeson',
                    href  : 'https://www.grails3book.com/',
                    about : 'Learn the fundamental concepts behind building Grails applications with the first book dedicated to Grails 3. Real, up-to-date code examples are provided, so you can easily follow along.',
                    image: 'pratical-grails-3-book-cover.png',
            ],
            [
                    title : 'Falando de Grails',
                    author: 'Henrique Lobo Weissmann',
                    href  : 'http://www.casadocodigo.com.br/products/livro-grails',
                    about : 'This is the best reference on Grails 2.5 and 3.0 written in Portuguese. It&#39;s a great guide to the framework, dealing with details that many users tend to ignore.',
                    image: 'grails_weissmann.png',
            ],
            [
                    title : 'Grails Goodness Notebook',
                    author: 'Hubert A. Klein Ikkink',
                    href  : 'https://leanpub.com/grails-goodness-notebook',
                    about : 'Experience the Grails framework through code snippets. Discover (hidden) Grails features through code examples and short articles. The articles and code will get you started quickly and provide deeper insight into Grails.',
                    image: 'grailsgood.png',
            ],
            [
                    title : 'The Definitive Guide to Grails 2',
                    author: 'Jeff Scott Brown and Graeme Rocher',
                    href  : 'http://www.apress.com/9781430243779',
                    about : 'As the title states, this is the definitive reference on the Grails framework, authored by core members of the development team.',
                    image: 'grocher_jbrown_cover.jpg',
            ],
            [
                    title : 'Grails in Action',
                    author: 'Glen Smith and Peter Ledbrook',
                    href  : 'http://www.manning.com/gsmith2/',
                    about : 'The second edition of Grails in Action is a comprehensive introduction to Grails 2 focused on helping you become super-productive fast.',
                    image: 'gsmith2_cover150.jpg',
            ],
            [
                    title : 'Grails 2: A Quick-Start Guide',
                    author: 'Dave Klein and Ben Klein',
                    href  : 'http://www.amazon.com/gp/product/1937785777?tag=misa09-20',
                    about : 'This revised and updated edition shows you how to use Grails by iteratively building a unique, working application.',
                    image : 'bklein_cover.jpg',
            ],
            [
                    title : 'Programming Grails',
                    author: 'Burt Beckwith',
                    href  : 'http://shop.oreilly.com/product/0636920024750.do',
                    about : 'Dig deeper into Grails architecture and discover how this application framework works its magic.',
                    image: 'bbeckwith_cover.gif'
            ]
    ] as List< Map<String, String> >

    public final static List< Map<String, String> > GROOVY_BOOKS = [
            [
                    title: 'Making Java Groovy',
                    author: 'Ken Kousen',
                    href: 'http://www.manning.com/kousen/',
                    about: 'Make Java development easier by adding Groovy. Each chapter focuses on a task Java developers do, like building, testing, or working with databases or restful web services, and shows ways Groovy can make those tasks easier.',
                    image: 'Kousen-MJG.png',
            ],
            [
                    title: 'Groovy in Action, 2nd Edition',
                    author: 'Dierk König, Guillaume Laforge, Paul King, Cédric Champeau, Hamlet D\'Arcy, Erik Pragt, and Jon Skeet',
                    href: 'http://www.manning.com/koenig2/',
                    about: 'This is the undisputed, definitive reference on the Groovy language, authored by core members of the development team.',
                    image: 'regina.png',
            ],
            [
                    title: 'Groovy for Domain-Specific Languages',
                    author: 'Fergal Dearle',
                    href: 'http://www.packtpub.com/groovy-for-domain-specific-languages-dsl/book',
                    about: 'Learn how Groovy can help Java developers easily build domain-specific languages into their applications.',
                    image: 'gdsl.jpg',
            ],
            [
                    title: 'Groovy 2 Cookbook',
                    author: 'Andrey Adamovitch, Luciano Fiandeso',
                    href: 'http://www.packtpub.com/groovy-2-cookbook/book',
                    about: 'This book contains more than 90 recipes that use the powerful features of Groovy 2 to develop solutions to everyday programming challenges.',
                    image: 'g2cook.jpg',
            ],
            [
                    title: 'Programming Groovy 2',
                    author: 'Venkat Subramaniam',
                    href: 'http://pragprog.com/book/vslg2/programming-groovy-2',
                    about: 'This book helps experienced Java developers learn to use Groovy 2, from the basics of the language to its latest advances.',
                    image: 'vslg2.jpg'
            ],
    ] as List< Map<String, String> >

    BookDataService bookDataService

    def init = { servletContext ->
        for (Map<String, String> bookInfo : (GRAILS_BOOKS + GROOVY_BOOKS)) {
            bookDataService.save(bookInfo.title, bookInfo.author, bookInfo.about, bookInfo.href, bookInfo.image)
        }
    }

    def destroy = {
    }
}

4.3 Your Services

Next we need to create our services to return all our books and our favourite ones. First we leverage data services for our basic functionality.

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

import grails.gorm.services.Service
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileDynamic
import org.grails.datastore.mapping.query.api.BuildableCriteria
import org.hibernate.transform.Transformers

interface IBookDataService {
    Book save(String title, String author, String about, String href, String image)
    Number count()
    Book findById(Long id)
}

@Service(Book)
abstract class BookDataService implements IBookDataService {

    @CompileDynamic
    @ReadOnly
    List<BookImage> findAll() {
        BuildableCriteria c = Book.createCriteria()
        c.list {
            resultTransformer(Transformers.aliasToBean(BookImage))
            projections {
                property('id', 'id')
                property('image', 'image')
            }
        }
    }

    @CompileDynamic
    @ReadOnly
    List<BookImage> findAllByIds(List<Long> ids) {
        BuildableCriteria c = Book.createCriteria()
        c.list {
            inList('id', ids)
            resultTransformer(Transformers.aliasToBean(BookImage))
            projections {
                property('id', 'id')
                property('image', 'image')
            }
        }
    }
}

Create a GORM Data Service for BookFavourite domain class CRUD operations.

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

import grails.gorm.services.Query
import grails.gorm.services.Service

@Service(BookFavourite)
interface BookFavouriteDataService {

    BookFavourite save(Long bookId, String username)

    @Query("select $b.bookId from ${BookFavourite b} where $b.username = $username") (1)
    List<Long> findBookIdByUsername(String username)
}

4.4 Your Controllers

After we are done creating our service layer we need to get our controllers in place. First we have our basic book controller that can either return a list of all books or show a selected book.

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

import grails.plugin.springsecurity.annotation.Secured
import groovy.transform.CompileStatic

@Secured('permitAll')
@CompileStatic
class BookController {

    static allowedMethods = [index: 'GET', show: 'GET']

    BookDataService bookDataService

    def index() {
        [bookList: bookDataService.findAll()]
    }

    def show(Long id) {
        [bookInstance: bookDataService.findById(id)]
    }

}

Next is our Favourites controller that can return a list of books we have favourited, and our action to favourite a desired book.

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

import grails.plugin.springsecurity.SpringSecurityService
import grails.plugin.springsecurity.annotation.Secured
import groovy.transform.CompileDynamic

class BookFavouriteController {

    static allowedMethods = [
            index: 'GET',
            favourite: 'POST',
    ]

    SpringSecurityService springSecurityService
    BookFavouriteDataService bookFavouriteDataService
    BookDataService bookDataService

    @Secured('permitAll')
    def index() {
        String username = loggedUsername()
        List<Long> bookIds = bookFavouriteDataService.findBookIdByUsername(username) (1)
        List<BookImage> bookList = bookDataService.findAllByIds(bookIds) (2)
        render view: '/book/index', model: [bookList: bookList]
    }

    @Secured('isAuthenticated()')
    def favourite(Long bookId) {
        String username = loggedUsername()
        bookFavouriteDataService.save(bookId, username) (3)
        redirect(action: 'index')
    }

    @CompileDynamic
    protected String loggedUsername() {
        springSecurityService.principal.username
    }

}
1 Call our custom favourite book finder
2 Call our custom findAll
3 Call to save our favourite

4.5 Your Views

We are finally ready to connect everything up with a functional interface. First lets create an index page for our books so we can see a list of them located at views/book.

grails-app/views/book/index.gsp
<html>
<head>
    <title>Groovy & Grails Books</title>
    <meta name="layout" content="main" />
</head>
<body>
<div id="content" role="main">
    <section class="row colset-2-its">
        <g:each in="${bookList}" var="${book}">
            <g:link controller="book" id="${book.id}" action="show">
                <asset:image src="${book.image}" width="200" />
            </g:link>
        </g:each>
    </section>
</div>
</body>
</html>

Next we will create a show.gsp also in views/books so that when we select a book we can view its details.

grails-app/views/book/show.gsp
<html>
<head>
    <title>${bookInstance?.title}</title>
    <meta name="layout" content="main" />
</head>
<body>
<div id="content" role="main">
    <section class="row colset-2-its">
        <g:if test="${bookInstance}">
            <h1><a href="${bookInstance.href}">${bookInstance.title}</a></h1>
            <sec:ifLoggedIn>
                <g:form controller="bookFavourite" action="favourite">
                    <g:hiddenField name="bookId" value="${bookInstance.id}"/>
                    <input type="submit" class="btn btn-default" value="${g.message(code: 'book.favourite', default: 'Favourite')}"/>
                </g:form>
            </sec:ifLoggedIn>
            <p>${bookInstance.about}</p>
            <h2><g:message code="book.author" args="[bookInstance.author]" default="By {0}"/></h2>
            <asset:image src="${bookInstance.image}" width="200" />
        </g:if>
    </section>
</div>
</body>
</html>

In the above code we wrap our favourite button in a logged in check as we should only be able to favourite a book when we are logged in

Lastly we tie it all together in our layout file by adding a simple menu that allows us to select either all our books, our favourite books, or login / logout. We add this between the navigation div and the <g:layoutBody/>.

grails-app/views/layouts/main.gsp
<div class="centered" style="margin: 10px auto;">
    <g:link controller="book" action="index">
        <g:message code="book.all" default="All"/>
    </g:link>
    <span>|</span>
    <g:link controller="bookFavourite" action="index">
        <g:message code="book.favourite" default="Favourites"/>
    </g:link>
    <span>|</span>
    <sec:ifNotLoggedIn>
        <g:render template="/auth/loginWithGoogle"/>
    </sec:ifNotLoggedIn>
    <sec:ifLoggedIn>
        <g:form controller="logout" style="display: inline;">
            <input type="submit" value="${g.message(code: "logout", default:"Logout")}"/>
        </g:form>
    </sec:ifLoggedIn>
</div>

<g:if test="${flash.message}">
    <div class="message" style="display: block">${flash.message}</div>
</g:if>

In the above code we:

  • Center our menu; link to list all books; link to list favourite books, login with google / logout

  • Include template with login link

  • Include template with logout link

  • Add a message block so we can view our login / logout messages

We create a template to contain our link to trigger our OAuth login through Google; the link /oauth/authenticate is provided from the spring-security-rest plugin and ends with the provider we are using /google for the link /oauth/authenticate/google. This will redirect you to the normal Google login / account selection you are already familiar with.

grails-app/views/auth/_loginWithGoogle.gsp
<a href="/oauth/authenticate/google">
    <asset:image src="[email protected]"
                 alt="${g.message(code: "login.google", default:"Login with Google")}" height="40"/>
</a>
[email protected] is already provided for you in assets

5 Running your Completed App

Run the app with Gradle bootRun task.

$ ./gradlew bootRun

as previously. Now that our app is running navigate to http://localhost:8080 to see the following.

homeScreen

Select a book and see that no Favourite button is available when not signed in.

unauthorizedShow

Click Login with Google from our menu and select your account / signin

googleSignin

See our successful login message

loginMessage

Select a book and see that the Favourite button is now available.

authorizedShow

Click on logout and you will see our logout message displayed.

logoutMessage

6 Next Steps

To further your understanding read through Grails Spring Security Rest and Spring Security Core documentation.

7 Do you need help with Grails?

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team