Play Silhouette and Social logins
Most users can’t be bothered to fill in a form with their email, fullname, same login as on every other website, and (the hardest part) two identical, complicated and strong passwords. Then every time they come back to you’re site, they have to login, which means reminding themselves which password they have used. One way of simplifying this is by authorizing users to your app by using social logins.
Play and Silhouette
Silhouette is a popular Scala authentication library for the Play Framework. It supports multiple authentication methods – including OAuth2. It also includes multiple pre configured Provider classes for popular social sites, which minimizes the amount of necessary work.
Configuration
First thing we need to configure is the state parameter
provider. We will use it to prevent CSRF attacks.
First step is adding some config keys and values to silhouette.conf
. Our snippet below presents configuration for both the CSRF state handler and the OAuth2 state provider – since we will be needing both of them.
# OAuth2 state provider settings oauth2StateProvider.cookieName="OAuth2TokenSecret" oauth2StateProvider.cookiePath="/" oauth2StateProvider.secureCookie=false oauth2StateProvider.httpOnlyCookie=true oauth2StateProvider.sameSite="Lax" oauth2StateProvider.expirationTime=5 minutes oauth2StateProvider.signer.key = "[changeme]" oauth2StateProvider.crypter.key = "[changeme]" # Social state handler socialStateHandler.signer.key = "[changeme]" # CSRF state item handler settings csrfStateItemHandler.cookieName="OAuth2State" csrfStateItemHandler.cookiePath="/" csrfStateItemHandler.secureCookie=false csrfStateItemHandler.httpOnlyCookie=true csrfStateItemHandler.sameSite="Lax" csrfStateItemHandler.expirationTime=5 minutes csrfStateItemHandler.signer.key = "[changeme]"
Signers
Next thing that needs to be done is creating Signer
s for the CSRF state item handler and OAuth2 state provider. They will be used to sign client side cookies. This implementation should look like the one in below and it should be placed in the same file as rest of Silhouette’s functions e.g. SilhouetteModule.scala
.
/** * Provides the signer for the social state handler. * * @param configuration The Play configuration. * @return The signer for the social state handler. */ @Provides @Named("social-state-signer") def provideSocialStateSigner(configuration: Configuration): Signer = { val config = configuration.underlying.as[JcaSignerSettings]("silhouette.socialStateHandler.signer") new JcaSigner(config) } /** * Provides the signer for the CSRF state item handler. * * @param configuration The Play configuration. * @return The signer for the CSRF state item handler. */ @Provides @Named("csrf-state-item-signer") def provideCSRFStateItemSigner(configuration: Configuration): Signer = { val config = configuration.underlying.as[JcaSignerSettings]("silhouette.csrfStateItemHandler.signer") new JcaSigner(config) }
Providers
After creating Signer
s, we can create both a CSRF state item handler and social state handler. First we will create a CSRF item handler.
/** * Provides the CSRF state item handler. * * @param idGenerator The ID generator implementation. * @param signer The signer implementation. * @param configuration The Play configuration. * @return The CSRF state item implementation. */ @Provides def provideCsrfStateItemHandler( idGenerator: IDGenerator, @Named("csrf-state-item-signer") signer: Signer, configuration: Configuration): CsrfStateItemHandler = { val settings = configuration.underlying.as[CsrfStateSettings]("silhouette.csrfStateItemHandler") new CsrfStateItemHandler(settings, idGenerator, signer) }
Now using this CSRF state item handler we will create the social state handler. We will use DefaultSocialStateHandler
as Silhouette documentation suggests that it’s the best option.
/** * Provides the social state handler. * * @param signer The signer implementation. * @return The social state handler implementation. */ @Provides def provideSocialStateHandler( @Named("social-state-signer") signer: Signer, csrfStateItemHandler: CsrfStateItemHandler): SocialStateHandler = { new DefaultSocialStateHandler(Set(csrfStateItemHandler), signer) }
Social media providers
Now that we have configured Silhouette for OAuth2 workflow and secured it against CSRF attacks we can create our social site providers. For the purpose of this article we will implement only Google provider, but other configurations are very similar. You can find sample configurations in the Silhouette documentation. Mostly likely you will want to use more than one provider for your users comfort. Code presented in this part of the article will be easily extensible.
Provider configuration
First thing that needs to be done is adding provider’s configuration to silhouette.conf
file. For Google it should look similar to the one below.
google { authorizationURL="https://accounts.google.com/o/oauth2/auth" accessTokenURL="https://accounts.google.com/o/oauth2/token" redirectURL=YOUR_APP_DOMAIN/authenticate/google" clientID="some_client_id" clientSecret="some_client_secret" scope="profile email" }
Note: You need to generate clientID and clientSecret in your Google account.
Next step is to create a GoogleProvider
. Again we will do this in SilhouetteModule
class. Code snippet below presents an implementation that will use the configuration stored in silhouette.conf
.
/** * Provides the Google provider. * * @param httpLayer The HTTP layer implementation. * @param socialStateHandler The social state handler implementation. * @param configuration The Play configuration. * @return The Google provider. */ @Provides def provideGoogleProvider( httpLayer: HTTPLayer, socialStateHandler: SocialStateHandler, configuration: Configuration): GoogleProvider = { new GoogleProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.google")) }
Now we could use just this provider in for further implementation, but for ease of extensibility we will create SocialProviderRegistry
. This will allow us to easily add more providers in future without the necessity to change the implementation of other classes. We can create this registry like this:
/** * Provides the social provider registry. * * @param facebookProvider The Facebook provider implementation. * @param googleProvider The Google provider implementation. * @param linkedInProvider The LinkedIn provider implementation. * @return The Silhouette environment. */ @Provides def provideSocialProviderRegistry( googleProvider: GoogleProvider // add more providers here ): SocialProviderRegistry = { SocialProviderRegistry(Seq( googleProvider // add more providers here )) }
Authentication Controller
Now that we have providers created we need to create an endpoint for authentication. First, to access our repository, we need to inject it to the controller, by adding @Inject()(socialProviderRegistry: SocialProviderRegistry)
to controller’s class. Next step is to create Action
that will sign in users, based on data sent by the provider. Method below takes the provider
string to determine which social provider was used by our user and use the correct one. Next it will try to authenticate the user based on using the previously determined provider. If it ends in success (Right) it will retrieve profile data sent by the social provider and will save it to the database (userService.save(profile)
is a custom function that you have to create by yourself). If service returns Some(user) which in our implementation means that data was correctly saved, it will create an authenticator and publish a LoginEvent
effectively logging in a new user.
/** * Social log in * * @param provider login provider */ def socialAuthenticate(provider: String) = silhouette.UnsecuredAction.async { implicit request => (socialProviderRegistry.get[SocialProvider](provider) match { case Some(p: SocialProvider with CommonSocialProfileBuilder) => p.authenticate().flatMap { case Left(result) => Future.successful(result) // Return authentication result with error case Right(authInfo) => (for { profile <- p.retrieveProfile(authInfo) optionalUser <- userService.save(profile) } yield optionalUser match { case Some(user) => for { authenticator <- silhouette.env.authenticatorService.create(user.loginInfo) value <- silhouette.env.authenticatorService.init(authenticator) result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.HomeController.index())) } yield { silhouette.env.eventBus.publish(LoginEvent(user, request)) result } case None => // Error handling while saving user Future.successful(Redirect(routes.AuthController.login).flashing("warning" -> Messages("auth.social.alreadyRegistered"))) }).flatten } case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider")) // Wrong provider key }).recover { // Any other unforeseen error handling case e: ProviderException => logger.error("Unexpected provider error", e) Redirect(routes.AuthController.login).flashing("warning" -> Messages("auth.social.couldNotAuthenticate")) } }
As with every endpoint we need to add it to the routes
configuration. It should look similar to this:
GET /authenticate/:provider controllers.AuthController.socialAuthenticate(provider: String)
Template
One last thing that needs to be done is allowing users to access this sign in method. We will do this by adding this snippet to the sign in template.
@if(socialProviders.providers.nonEmpty) { <div class="social-providers mt-2"> <p class="col-sm-12 justify-content-center d-flex">@messages("auth.signIn.useSocial")</p> <div class="col-sm-12 justify-content-center d-flex"> @for(p <- socialProviders.providers) { <a href="@controllers.routes.AuthController.socialAuthenticate(p.id)" class="social-auth provider @p.id" title="@messages(p.id)"><i aria-hidden="true" class="fab fa-@{p.id} font-50 m-2"></i></a> } </div> </div> }
This will create links to all social providers from our Repository. class="fab fa-@{p.id} font-50 m-2"
will fetch provider icons from FontAwesome.