Implement OAuth-based Social Network Logins in Grails
By Piotr Jagielski
Grails Spring Security is a great plugin. It allows you to set up authorization for your app in just a few lines in Grails configuration. So, users can register to your website, click on a confirmation link received by e-mail and login with username and password. User experience? Another password to remember or even worse – another public website with the same password provided...
So here comes OAuth. As almost everybody on the planet has an account on one of the social networks, why can't we ask this network to authorize the user of our application? A user logs in with his/her credentials to Facebook/Google+/whatever (called OAuth provider) and gives access permission to our application. OAuth provider redirects to your website saying that user has been authorized and gives you some basic information about the user, with no password being stored in your application's database!
OK, so we have Facebook and Twitter authentication plugins, but what about other providers? Recently, I've released a website for developers. Do all developers have Facebook accounts? Here are stats from my site:
- Google+ 52%
- Facebook 17%
- Twitter: 16%
- GitHub: 10%
- LinkedIn 5%
All of these are OAuth providers. But what about the Grails' plugins?
After Googling a bit I've found Grails' Inviter plugin by Tomas Lin. It allows you to import your friends' contacts and send them an e-mail invitation to your website. Underneath, the plugin uses the scribe-java library – an open source implementation of OAuth 1/2 protocol. So why not use scribe-java for the authentication of users from various social networks in your application?
Scribe is sort of a low-level implementation of OAuth. Here's a list of its main elements:
- ServiceBuilder - an abstract factory that lets you build service for a concrete OAuth provider, giving your app apiKey and apiSecret.
- Verifier - OAuth verifier code sent by the OAuth provider, which is used to create the access token.
- OAuthRequest - request to OAuth provider, which is signed by the access token.
You can look at the Getting Started page for more detail.
To implement authentication we need to implement the following:
- With ServiceBuilder create and redirect to the authorization URL to OAuth provider.
- Implement callback action, which extracts an access token from a verifier code given by the OAuth provider.
- Create and sign an OAuth request to receive user information (*).
- Inject user principal into the Spring Security context.
(*) - you can also extract user data, e.g. from a Facebook cookie, but using a separate OAuth request is much more readable and compatible with all providers.
Here is a diagram showing the details of communication between browser, out controller (AuthController), scribe library and OAuth provider:
OK, so we would write AuthController to handle signin by the social account and callback from OAuth provider:
class AuthController { SpringSecuritySigninService springSecuritySigninService def signin = { GrailsOAuthService service = resolveService(params.provider) if (!service) { redirect(url: '/') } session["${params.provider}_originalUrl"] = params.originalUrl def callbackParams = [provider: params.provider] def callback = "${createLink(action: 'callback', absolute: 'true', params: callbackParams)}" def authInfo = service.getAuthInfo(callback) session["${params.provider}_authInfo"] = authInfo redirect(url: authInfo.authUrl) } def callback = { GrailsOAuthService service = resolveService(params.provider) if (!service) { redirect(url: '/') } AuthInfo authInfo = session["${params.provider}_authInfo"] def requestToken = authInfo.requestToken def accessToken = service.getAccessToken(authInfo.service, params, requestToken) session["${params.provider}_authToken"] = accessToken def profile = service.getProfile(authInfo.service, accessToken) session["${params.provider}_profile"] = profile def uid = profile.uid User user = User.findByOauthIdAndOauthProvider(uid, params.provider) if (user) { springSecuritySigninService.signIn(user) redirect(uri: (session["${params.provider}_originalUrl"] ?: '/') - request.contextPath) } else { redirect(controller: 'user', action: 'registerOAuth', params: params) } } private def resolveService(provider) { def serviceName = "${provider as String}AuthService" grailsApplication.mainContext.getBean(serviceName) } }
As you can see - besides storing some data into the session, the main logic is implemented in GrailsOAuthService class - a kind of wrapper for the scribe-java api.
Here is a sample implementation for Google+:
class GoogleAuthService extends GrailsOAuthService { @Override OAuthService createOAuthService(String callbackUrl) { def builder = createServiceBuilder(GoogleApi20, grailsApplication.config.auth.google.key as String, grailsApplication.config.auth.google.secret as String, callbackUrl) return builder.grantType(OAuthConstants.AUTHORIZATION_CODE) .scope('https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email') .build() } AuthInfo getAuthInfo(String callbackUrl) { OAuthService authService = createOAuthService(callbackUrl) new AuthInfo(authUrl: authService.getAuthorizationUrl(null), service: authService) } Token getAccessToken(OAuthService authService, Map params, Token requestToken) { Verifier verifier = new Verifier(params.code) authService.getAccessToken(requestToken, verifier) } } public abstract class GrailsOAuthService { static transactional = false OAuthService oauthService def grailsApplication @Override ServiceBuilder createServiceBuilder(Class provider, String apiKey, String secretKey, String callbackUrl) { def ServiceBuilder builder = new ServiceBuilder().provider(provider) .apiKey(apiKey).apiSecret(secretKey) .callback(callbackUrl) return builder } abstract AuthInfo getAuthInfo(String callbackUrl) abstract Token getAccessToken(OAuthService authService, Map params, Token requestToken) abstract OAuthProfile getProfile(OAuthService authService, Token accessToken) } class AuthInfo { OAuthService service String authUrl Token requestToken }
Last but not least, we need to extract the user data by accessing the providers specific URL with a request signed with an access token. URLs are listed in the table below:
PROVIDER |
URL |
|
https://graph.facebook.com/me |
Google+ |
https://www.googleapis.com/oauth2/v1/userinfo |
|
http://api.twitter.com/1/account/verify_credentials.json |
|
http://api.linkedin.com/v1/people/~ |
GitHub |
https://github.com/api/v2/json/user/show |
The structure of the returned user profile is also provider specific - mostly returned by some JSON object. Here's example code for Google+:
class AuthController { SpringSecuritySigninService springSecuritySigninService def signin = { GrailsOAuthService service = resolveService(params.provider) if (!service) { redirect(url: '/') } session["${params.provider}_originalUrl"] = params.originalUrl def callbackParams = [provider: params.provider] def callback = "${createLink(action: 'callback', absolute: 'true', params: callbackParams)}" def authInfo = service.getAuthInfo(callback) session["${params.provider}_authInfo"] = authInfo redirect(url: authInfo.authUrl) } def callback = { GrailsOAuthService service = resolveService(params.provider) if (!service) { redirect(url: '/') } AuthInfo authInfo = session["${params.provider}_authInfo"] def requestToken = authInfo.requestToken def accessToken = service.getAccessToken(authInfo.service, params, requestToken) session["${params.provider}_authToken"] = accessToken def profile = service.getProfile(authInfo.service, accessToken) session["${params.provider}_profile"] = profile def uid = profile.uid User user = User.findByOauthIdAndOauthProvider(uid, params.provider) if (user) { springSecuritySigninService.signIn(user) redirect(uri: (session["${params.provider}_originalUrl"] ?: '/') - request.contextPath) } else { redirect(controller: 'user', action: 'registerOAuth', params: params) } } private def resolveService(provider) { def serviceName = "${provider as String}AuthService" grailsApplication.mainContext.getBean(serviceName) } }
The last part is injecting the principal object into Spring Security. Suppose we use the standard grails-spring-security model classes to store user data, this is an example code:
OAuthProfile getProfile(OAuthService authService, Token accessToken) { OAuthRequest request = new OAuthRequest(Verb.GET, 'https://www.googleapis.com/oauth2/v1/userinfo') authService.signRequest(accessToken, request) def response = request.send() def user = JSON.parse(response.body) def login = "${user.given_name}.${user.family_name}".toLowerCase() new OAuthProfile(username: login, email: user.email, uid: user.id, picture: user.picture) }
And we have fully authenticated UserDetails object with @Secured annotation compatible authorities.
The Grails project with all source code is hosted at GitHub: https://github.com/TouK/grails-oauth-scribe-example. There is also a working demo on CloundFoundry: http://grails-oauth-scribe-example.cloudfoundry.com/
Enjoy!
(This article is based on a blog post on http://grailsblog.pl and http://touk.pl/blog)
About the Author
Piotr Jagielski is a Lead Developer at TouK.
This article was originally published on August 2, 2012