Twitter OAuth with Grails 3 and Spring Security REST
Learn how to use Twitter OAuth with Grails 3 and Spring Security REST plugin
Authors: Ben Rhine, Sergio del Amo
Grails Version: 3.3.5
1 Getting Started
In this guide we will show you how to add Twitter OAuth2 authentication to your app 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.8 or greater installed with
JAVA_HOME
configured appropriately
1.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-oauth-twitter.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-oauth-twitter/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-twitter/complete
|
If you want to start from scratch, create a new Grails 3 application using Grails Application Forge.
2 OAuth 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 Twitter so in more laymen’s term a Twitter user approves their account to issue access tokens back to the requesting application.
2.1 Setup and Configure Twitter OAuth
To get Twitter OAuth up and running on your application will take a bit of work and configuration on the
Twitter Developer Console. Go ahead and sign in and click Apply
Select Get started with standard access
This gives you a quick description of how to connect your app to Twitter. Click on the link apps.twitter.com
.
This will bring you to Twitters Application Management
From the Twitter Application Management page click Create New App
Twitter does not accept localhost so give it the localhost ip address instead 127.0.0.1 .
|
Fill out the form with a app name, basic description, your web address, and a callback (where you want Twitter to
return to after you signin). Then click Create your Twitter application
.
You should now see that your app was created successfully with Twitter.
Next select the Keys and Access Tokens
tab
This is all the setup that needs to be done in Twitter. Save your Consumer Key and Secret for quick access later. Now we will take a look at setting up our application with our Twitter credentials.
3 Setting up your app
With all our Twitter configuration in place its time get our app configured to use security and connect using OAuth2 over REST through Twitter.
The next diagramm describes the security solution we are going to implement.
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.
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
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
.
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.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 {
plugin {
springsecurity {
rest {
token {
validation {
useBearerToken = false (1)
enableAnonymousAccess = true (2)
}
storage {
jwt {
secret = 'foobar123'*4 (3)
}
}
}
oauth {
frontendCallbackUrl = { String tokenValue -> "http://127.0.0.1:8080/auth/success?token=${tokenValue}" } (4)
twitter {
client = org.pac4j.oauth.client.TwitterClient (5)
key = '${TWITTER_KEY}' (6)
secret = '${TWITTER_SECRET}' (7)
defaultRoles = [] (8)
}
}
}
providerNames = ['anonymousAuthenticationProvider'] (9)
}
}
}
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 authentication with Twitter, this callback url will be invoked with a JWT token which authenticates the user |
5 | Which pac4j client to use; in our case the Twitter client |
6 | Supply your Twitter Consumer Key (API Key) as the System Property TWITTER_KEY when you start the app. |
7 | Supply your Twitter Consumer Secret (API Secret) as the System Property TWITTER_SECRET when you start the app. |
8 | Specific roles that Twitter authenticated users get. |
9 | We are going to authenticate our users only against Twitter. 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 -DTWITTER_KEY=XXXXXX -DTWITTER_SECRET=XXXX bootRun
We want our app stateless by default with some endpoints which allow anonymous access.
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/twitter', filters: ANONYMOUS_FILTERS], (1)
[pattern: '/oauth/callback/twitter', 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.
<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 Twitter 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
:
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.plugin.springsecurity.logout.handlerNames = ['rememberMeServices', 'securityContextLogoutHandler', 'cookieClearingLogoutHandler']
3.5 JWT Cookie
Create AuthController.groovy
. When the user logs in successfully with Twitter, AuthController.success
is invoked.
The path to AuthController.success is used in frontendCallbackUrl in application.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(httpOnly()) (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.
<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 Twitter OAuth api, add the
following to the end of your logback.groovy
file
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 Twitter.
4.1 Your Domain
For your domain we will need to domain objects Book
and BookFavourite
. Create as follows
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'
}
}
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. |
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'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.
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.
package demo
import grails.gorm.services.Query
import grails.gorm.services.Service
@Service(BookFavourite)
interface BookFavouriteDataService {
BookFavourite save(Long bookId, String username)
void delete(Long bookId, String username)
@Query("select $b.bookId from ${BookFavourite b} where $b.username = $username") (1)
List<Long> findBookIdByUsername(String username)
BookFavourite findByBookIdAndUsername(Long bookId, String username)
}
1 | You can use JPA-QL Queries with GORM Data Services. |
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.
package demo
import grails.plugin.springsecurity.SpringSecurityService
import grails.plugin.springsecurity.annotation.Secured
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
@Secured('permitAll')
@CompileStatic
class BookController {
static allowedMethods = [index: 'GET', show: 'GET']
BookDataService bookDataService
BookFavouriteDataService bookFavouriteDataService
SpringSecurityService springSecurityService
def index() {
[bookList: bookDataService.findAll()]
}
def show(Long id) {
[
bookInstance: bookDataService.findById(id),
bookFavourite: bookFavouriteDataService.findByBookIdAndUsername(id, loggedUsername())
]
}
@CompileDynamic
protected String loggedUsername() {
final String username = springSecurityService.principal?.username
username
}
}
Next is our Favourites controller that can return a list of books we have favourited, and our action to favourite a desired book.
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',
unfavourite: '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')
}
@Secured('isAuthenticated()')
def unfavourite(Long bookId) {
String username = loggedUsername()
bookFavouriteDataService.delete(bookId, username)
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
.
<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.
<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="${bookFavourite != null ? 'unfavourite' : 'favourite'}">
<g:hiddenField name="bookId" value="${bookInstance.id}"/>
<g:if test="${bookFavourite}">
<input type="submit" class="btn btn-default" value="${g.message(code: 'book.unfavourite', default: 'Unfavourite')}"/>
</g:if>
<g:else>
<input type="submit" class="btn btn-default" value="${g.message(code: 'book.favourite', default: 'Favourite')}"/>
</g:else>
</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/>
.
<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/loginWithTwitter"/>
</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, "sign in with twitter" / 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 Twitter; the link /oauth/authenticate
is provided from the spring-security-rest
plugin and ends with the provider we are using /twitter
for the
link /oauth/authenticate/twitter
. This will redirect you to the normal Twitter login page that you are already familiar with.
<a href="/oauth/authenticate/twitter">
<asset:image src="sign-in-with-twitter-link.png"
alt="${g.message(code: "login.twitter", default:"Login with Twitter")}" height="10"/>
</a>
sign-in-with-twitter-link.png 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.
Select a book and see that no Favourite
button is available when not signed in.
Click Login with Twitter
from our menu and enter your twitter credentials:
After you are succesfully redirected, you can inspect and discover the JWT token stored as a Cookie:
Select a book and see that the Favourite
button is now available.
Click on logout and you will see our logout message displayed.
6 Next Steps
To further your understanding read through Grails Spring Security Rest and Spring Security Core documentation.