Social login is a common task nowadays. As usual, “Play” already has a very good plugin to make this task a little bit easier for us. I am talking about Secure Social. This post is more a tutorial than a discussion about some topic, let’s go through all steps to have a working project :).
First you need to add the dependency.
libraryDependencies ++= Seq( javaJdbc, javaJpa, cache, "ws.securesocial" %% "securesocial" % "2.1.3"
Now, let’s add a link with the authentication address. In this tutorial we will use facebook as a social provider, but you can replicate the example for all other social networks. But what is the link that have to be put in our templates? Don’t worry, Secure Social already has some builtin controllers that can handle this part for you. Your link should be like that:
href="@securesocial.controllers.routes.ProviderController.authenticate("facebook")"
Probably your console is showing a compile error now, it’s ok, we didn’t configure the routes. Let’s fix that now:
# Login page GET /logout securesocial.controllers.LoginPage.logout # Providers entry points GET /authenticate/:provider securesocial.controllers.ProviderController.authenticate(provider)
After your user clicks on the link, facebook will authenticate and send him back to your application. Probably you will need to store this user in a database or recover his informations to make it available around your application. Thinking about this, Secure Social obligates you implement a class to handle these situations.
public class FacebookUserService extends BaseUserService { public FacebookUserService(Application application) { super(application); } @Override public Identity doFind(final IdentityId id) { return SiteUsers.socialFind(id.providerId(), id.userId()); } @Override public Identity doSave(final Identity identity) { SiteUser newUser = new SiteUser(avatarUrl, email, identity.firstName(), identity.identityId().userId(), identity.identityId().providerId()); JPA.em().persist(newUser); //compilation error here... return newUser; } //other methods here }
For social networks you just need to implement doFind and doSave. All other methods are for users who are using the DatabaseProvider.
There is one thing here that needs attention, method doSave is always called after a login. So you have to verify in your database if you need to store or just return an existing user. Another important part is that we are using JPA plugin and this plugin obligates you to run every JPA operation inside a transaction. Problem is that Secure Social’s controllers don’t know anything about that, so they don’t use @Transactional. To fix that, we can use JPA.withTransaction method and pass a function to run inside a transaction.
@Override public Identity doSave(final Identity identity) { Identity oldUser = FacebookUserService.this.doFind(identity .identityId()); if(oldUser!=null){ return oldUser; } final String email = identity.email().isDefined() ? identity.email() .get() : null; final String avatarUrl = identity.avatarUrl().isDefined() ? identity .avatarUrl().get() : null; SiteUser user = <em>JPA.withTransaction</em>(new Function0() { @Override public SiteUser apply() throws Throwable { SiteUser newUser = new SiteUser(avatarUrl, email, identity .firstName(), identity.identityId().userId(), identity .identityId().providerId()); JPA.em().persist(newUser); return newUser; } }); return new WrapIdentity(user); }
One other thing that you maybe have noticed is the fact that we return Identity in our methods. Here you have to options: make your User class implement this interface or create a Wrapper and do not mix your model class with plugin’s code. I prefer the second one, so here is my implementation:
public class WrapIdentity implements Identity { private SiteUser user; public WrapIdentity(SiteUser user) { this.user = user; } public SiteUser getUser(){...} //methods here }
You just have to change the return in doFind and doSave methods.
Finally, the last two steps!. In order to have everything working is necessary to create a application in facebook and, after that, configure the public an private key. To not mix your application configuration with your social login configuration, is recommended to put these informations in a separated file.
securesocial { onLoginGoTo=/ onLogoutGoTo=/ facebook { authorizationUrl="https://graph.facebook.com/oauth/authorize" accessTokenUrl="https://graph.facebook.com/oauth/access_token" clientId=xxxx clientSecret=xxxx # this scope is the minimum SecureSocial requires. You can add more if required by your app. scope=email } }
We configured two more things here, onLoginGoTo and onLogoutGoTo keys. It’s important because Secure Social needs to know where it should go after login and logout operations. Do not forget to include this file in your application.conf! One more note here is about your facebook app. Maybe it will be good for you create two apps, one for test purposes and other for production. In test app, configure back url to point localhost address :).
Secure Social is a plugin, so you have to configure it in order to play loads all needed classes. Just to remember, this configuration is in play.plugins file.
9994:securesocial.core.DefaultAuthenticatorStore 9995:securesocial.core.DefaultIdGenerator 9997:securesocial.controllers.DefaultTemplatesPlugin 9998:security.FacebookUserService 10001:securesocial.core.providers.FacebookProvider
Besides Secure Social classes, we have to add our implementation. In my case the class name is FacebookUserService.
Let’s get into the last part of this post. Probably you will need to show information about the logged user and, maybe, secure some actions. For the first thing, you need to get current user. In order to do this you can use SecureSocial.currentUser method. But here you have a problem: How are you going to access this class in your template? Pass this object around all your templates as a parameter will be problematic. My tip is to create a helper and import this class as default for all templates.
public class CurrentSiteUser { public static Option apply(){ WrapIdentity identity = (WrapIdentity) SecureSocial.currentUser(); return Option.apply(identity); } } //build.sbt templatesImport ++= Seq("helpers.CurrentSiteUser")
The usage is simple now.
@CurrentSiteUser.apply().map { identity => your html code for logged user here }
If you need to protect some action, just add a annotation. And you can configure for ajax call as well!
@SecuredAction(ajaxCall = true) public static Promise likePhoto(final String instagramId) { }
AjaxCall attribute is great because Secure Social returns a nice json response if you are trying to make a ajax call.
That is it! If you have any doubt about this post leave a question and I will be happy in help you. For more information about Play, you can keep your eyes here and give a try for my book, available in Leanpub 🙂
With securesocial 3.0-M1, ProviderController does not have a companion object anymore, and I had to define the route entries as follows:
# Login page
GET /logout @securesocial.controllers.LoginPage.logout
# Providers entry points
GET /authenticate/:provider @securesocial.controllers.ProviderController.authenticate(provider)
Because of that, links like the one used in your blog can’t be used (i.e.: href=”@securesocial.controllers.routes.ProviderController.authenticate(“facebook”)” ).
How to create the right urls with reverse routing?
Hi Daniel,
I did not give a try to the last release of Secure Social. I am going to check in order to understand what is needed :).