[Solved] Authenticate using access token from Facebook android login


#1

Hi everyone,

For our app we use Facebook login on Android and we’d like to somehow use that to authenticate on the server. Is that possible ?
To be more precise what I thought is that we could use the token we get from Android and then send it to the server, the server would then be able to retrieve user data as if the user authenticated on the server directly (by being redirected to the Facebook login page).

Thanks in advance for your help, and thank you for this awesome framework !


#2

Hi,

your description is a bit vague! What token do you will use to authenticate against the server? The Facebook access token or a token generated by an Android device? What user data do you try to retrieve? Data from Facebook or data from your backend?

Best regards,
Christian


#3

I’m using the Facebook access token that I get after the user logs in on our Android app, as described here: Facebook Login pour Android.

Both actually, but the idea was to go through the server each time. So mainly data from my backend.

To sum up, instead of the user going on the sign in page, being redirected to the Facebook website and then being recognized as authenticated, he would provide the token (he got from Facebook) in some header to achieve the same result.
On subsequent requests, he’d be recognized as logged in.

I actually came up with a solution in the meantime. I added a provider that subclasses the Facebook provider and overrides its authenticate method. What do you think ?

request.extractString(AccessToken) match {

            case Some(token) =>
                Future(Right(
                    OAuth2Info(
                        accessToken = token,
                        tokenType = request.extractString(TokenType),
                        expiresIn = request.extractString(ExpiresIn).map(_.toInt),
                        refreshToken = request.extractString(RefreshToken)
                    )
                ))
            case _ => super.authenticate()
 }

#4

I would not use the Facebook access token to authenticate a user on subsequent requests against the backend. The access token is a sensitive value which should be kept as a secret. Instead I would create an API endpoint which gets the access token and returns an authenticator. So after you get the access token from Facebook you send it to this endpoint. The endpoint validates the token by making an request to the Facebook API. If the token is valid, then you can create an authenticator(JWT, Bearer Token, Cookie) and send it back to the Android app. Now your Android app can make requests to the backend with an authenticator instead of the access token. If the authenticator expires, you should show the user the sign-in view again.


#5

I wanted to avoid doing this myself because I wanted to reuse as much as I could what was already done.
That’s why I subclassed the FacebookProvider. This allows me to bypass the process of going through the web page, being redirected to Facebook, … and providing the token directly, letting the core library take hand and do its usual job from there (retrieve the user profile from facebook, …).

That’s actually what I’m doing. I inject my custom provider in the provider registry and the rest is exactly like the normal authentication procedure (the one used in the Play seed template):

//The custom Facebook provider is injected in the registry
def authenticate(provider: String) = Action.async { implicit request =>
        (socialProviderRegistry.get[SocialProvider](provider) match {
            case Some(p: SocialProvider with CommonSocialProfileBuilder) =>
                p.authenticate().flatMap {
                    case Left(result) => Future.successful(result)
                    case Right(authInfo) => for {
                        profile <- p.retrieveProfile(authInfo)
                        user <- userService.save(profile)
                        authInfo <- authInfoRepository.save(profile.loginInfo, authInfo)
                        authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo)
                        value <- silhouette.env.authenticatorService.init(authenticator)
                        result <- silhouette.env.authenticatorService.embed(value, Redirect(routes.Application.index()))
                    } yield {
                        silhouette.env.eventBus.publish(LoginEvent(user, request))
                        result
                    }
                }
            case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
        }).recover {
            case e: ProviderException =>
                logger.error("Unexpected provider error", e)
                NotFound
        }
    }

And this is the whole custom Facebook provider:

class FacebookTokenProvider(httpLayer: HTTPLayer, stateProvider: OAuth2StateProvider, settings: OAuth2Settings)
        extends FacebookProvider(httpLayer, stateProvider, settings) {

    override val id: String = "facebooktoken"

    /**
      * Starts the authentication process.
      *
      * @param request The current request.
      * @tparam B The type of the request body.
      * @return Either a Result or the auth info from the provider.
      */
    override def authenticate[B]()(implicit request: ExtractableRequest[B]): Future[Either[Result, OAuth2Info]] = {
        request.extractString(AccessToken) match {
            case Some(token) =>
                Future(Right(
                    OAuth2Info(
                        accessToken = token,
                        tokenType = request.extractString(TokenType),
                        expiresIn = request.extractString(ExpiresIn).map(_.toInt),
                        refreshToken = request.extractString(RefreshToken)
                    )
                ))
            case _ => super.authenticate()
        }
    }

}

Does this seem like a good flow to continue with or are there better solutions ?
Thanks for your help !


#6

Seems OK to me, expect the fact that you do not need to override the FacebookProvider. Every SocialProvider has retrieveProfile method which you can use to query the profile with the AuthInfo. So instead of constructing the AuthInfo in the authenticate method of the FacebookTokenProvider. You can construct it outside(where you call the provider) and pass it to the FacebookProvider.retrieveProfile method.


#7

Very good idea ! This way we could also easily authenticate against any provider without having to create a subclass for each.

Here’s what I came up with, maybe it’ll be usefull for others:

/*
OAuthInfoFromToken extracts the token from the request (as seen in the authenticate method of FacebookTokenProvider)
It then returns optionally an OAuth2Info with the info
*/
def authenticateToken(provider: String) = Action.async { implicit request =>
        ( (socialProviderRegistry.get[SocialProvider](provider), OAuthInfoFromToken()) match {
            case ( Some(p: OAuth2Provider with CommonSocialProfileBuilder), Some(authInfo) ) =>
                for {
                    profile <- p.retrieveProfile(authInfo)
                    user <- userService.save(profile)
                    authInfo <- authInfoRepository.save(profile.loginInfo, authInfo)
                    authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo)
                    value <- silhouette.env.authenticatorService.init(authenticator)
                    result <- silhouette.env.authenticatorService.embed(value, Ok)
                } yield {
                    silhouette.env.eventBus.publish(LoginEvent(user, request))
                    result
                }

            case (_, None) =>
                Future.failed(
                    new OAuth2StateException(s"No token found in the request while authenticating with $provider")
                )
            case _ =>
                Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
        }).recover {
            case e: OAuth2StateException =>
                logger.error("Unexpected token error", e)
                NotFound
            case e: ProviderException =>
                logger.error("Unexpected provider error", e)
                NotFound
        }
    }

#8

Thanks, that’s exactly what I was trying to do !