Cannot login with Facebook with play framework, silhouette, and satellizer

256 Views Asked by At

I was doing just fine this morning when I realized that the facebook login wasn't working anymore in my app. As I mentioned I'm using play framework with silhouette version 2.0-RC1.

This is the problem:

[Silhouette][facebook] Cannot build OAuth2Info because of invalid response format  : {"access_token":"EAAE2YyQkAUUBANAfoUfhdG8jrRfJnhrgZCaZB5FsZAO1G5Jq0ITTfZA6coj4g0HuUC48JToHCnZCFx8r9Q3JZCulzt6SaEcRqKrxNealsBldH4dpzJpK4oeblZAmxjq9Rjzl2rO3IKPUwllWAv5vEz33cGc4XeqLSgZD","token_type":"bearer","expires_in":5183717}

The problem is that the Json response for the authentication doesn't have the correct format as OAuth2Info expects, which is weird because we never touch the authentication mecanism and I'm woried because it could be a library version problem and update silhouette is not an option right now... Is somebody having this problem and found the solution? Please help me with this guys... Thanks a lot.

1

There are 1 best solutions below

1
Juan Sebastian Martinez On

We have to create a custom implementation of the FacebookProvider in order to process the new facebook's answer. Here it is

    import com.mohiva.play.silhouette.api.LoginInfo
    import com.mohiva.play.silhouette.api.exceptions.AuthenticationException
    import com.mohiva.play.silhouette.api.util.HTTPLayer
    import com.mohiva.play.silhouette.impl.exceptions.ProfileRetrievalException
    import com.mohiva.play.silhouette.impl.providers.OAuth2Provider._
    import com.mohiva.play.silhouette.impl.providers._
    import com.mohiva.play.silhouette.impl.providers.oauth2.{FacebookProfileBuilder, FacebookProvider}
    import com.mohiva.play.silhouette.impl.providers.oauth2.FacebookProvider._
    import play.api.libs.concurrent.Execution.Implicits._
    import play.api.libs.json.{ JsObject, JsValue }
    import play.api.libs.ws.WSResponse
    import play.api.libs.json._

    import scala.concurrent.Future
    import scala.util.{ Failure, Success, Try }

    /**
     * A Facebook OAuth2 Provider.
     *
     * @param httpLayer The HTTP layer implementation.
     * @param stateProvider The state provider implementation.
     * @param settings The provider settings.
     *
     * @see https://developers.facebook.com/tools/explorer
     * @see https://developers.facebook.com/docs/graph-api/reference/user
     * @see https://developers.facebook.com/docs/facebook-login/access-tokens
     */
    abstract class FacebookProvider(httpLayer: HTTPLayer, stateProvider: OAuth2StateProvider, settings: OAuth2Settings)
      extends OAuth2Provider(httpLayer, stateProvider, settings) {

      /**
       * The content type to parse a profile from.
       */
      type Content = JsValue

      /**
       * Gets the provider ID.
       *
       * @return The provider ID.
       */
      val id = ID

      /**
       * Defines the URLs that are needed to retrieve the profile data.
       */
      protected val urls = Map("api" -> API)

      /**
       * Builds the social profile.
       *
       * @param authInfo The auth info received from the provider.
       * @return On success the build social profile, otherwise a failure.
       */
      protected def buildProfile(authInfo: OAuth2Info): Future[Profile] = {
        httpLayer.url(urls("api").format(authInfo.accessToken)).get().flatMap { response =>
          val json = response.json
          (json \ "error").asOpt[JsObject] match {
            case Some(error) =>
              val errorMsg = (error \ "message").as[String]
              val errorType = (error \ "type").as[String]
              val errorCode = (error \ "code").as[Int]

              throw new ProfileRetrievalException(SpecifiedProfileError.format(id, errorMsg, errorType, errorCode))
            case _ => profileParser.parse(json)
          }
        }
      }

      /**
       * Builds the OAuth2 info from response.
       *
       * @param response The response from the provider.
       * @return The OAuth2 info on success, otherwise a failure.
       */
      override def buildInfo(response: WSResponse): Try[OAuth2Info] = {
        val responseJson = Json.parse(response.body)
        responseJson.validate[OAuth2Info].asEither.fold(
          error => Failure(new AuthenticationException(InvalidInfoFormat.format(id, error))),
          info => Success(info)
        )
      }
    }

    /**
     * The profile parser for the common social profile.
     */
    class FacebookProfileParser extends SocialProfileParser[JsValue, CommonSocialProfile] {

      /**
       * Parses the social profile.
       *
       * @param json The content returned from the provider.
       * @return The social profile from given result.
       */
      def parse(json: JsValue) = Future.successful {
        val userID = (json \ "id").as[String]
        val firstName = (json \ "first_name").asOpt[String]
        val lastName = (json \ "last_name").asOpt[String]
        val fullName = (json \ "name").asOpt[String]
        val avatarURL = (json \ "picture" \ "data" \ "url").asOpt[String]
        val email = (json \ "email").asOpt[String]

        CommonSocialProfile(
          loginInfo = LoginInfo(ID, userID),
          firstName = firstName,
          lastName = lastName,
          fullName = fullName,
          avatarURL = avatarURL,
          email = email)
      }
    }

    /**
     * The profile builder for the common social profile.
     */
    trait FacebookProfileBuilder extends CommonSocialProfileBuilder {
      self: FacebookProvider =>

      /**
       * The profile parser implementation.
       */
      val profileParser = new FacebookProfileParser
    }

    /**
     * The companion object.
     */
    object FacebookWesuraProvider {

      /**
       * The error messages.
       */
      val SpecifiedProfileError = "[Silhouette][%s] Error retrieving profile information. Error message: %s, type: %s, code: %s"

      /**
       * The Facebook constants.
       */
      val ID = "facebook"
      val API = "https://graph.facebook.com/me?fields=name,first_name,last_name,picture,email&return_ssl_resources=1&access_token=%s"

      /**
       * Creates an instance of the provider.
       *
       * @param httpLayer The HTTP layer implementation.
       * @param stateProvider The state provider implementation.
       * @param settings The provider settings.
       * @return An instance of this provider.
       */
      def apply(httpLayer: HTTPL

ayer, stateProvider: OAuth2StateProvider, settings: OAuth2Settings) = {
    new FacebookProvider(httpLayer, stateProvider, settings) with FacebookProfileBuilder
  }
}