Problems mixing JWT and Facebook OAuth2


#1

Hello everyone,

I’m having an hard time wrapping my head around using together JWT and OAuth2.
I’ll copy and paste my Module.scala, hoping someone will spot what’s wrong.

I’m guessing that provideOAuth2StateProvider perhaps shouldn’t be returning a cookie, since I’m using JWT?
Well, I must admit I’m VERY confused.
By the way, JWT Auth is working just fine.

I’m using Play 2.5 and Silhouette 4.0.

package module

import com.google.inject.name.Named
import com.google.inject.{ AbstractModule, Provides }
import com.mohiva.play.silhouette.api.{ Environment, EventBus, Silhouette, SilhouetteProvider }
import com.mohiva.play.silhouette.api.actions.SecuredErrorHandler
import com.mohiva.play.silhouette.api.crypto.Base64AuthenticatorEncoder
import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository
import com.mohiva.play.silhouette.api.services.{ AuthenticatorService, AvatarService, IdentityService }
import com.mohiva.play.silhouette.api.util._
import com.mohiva.play.silhouette.impl.authenticators.{ JWTAuthenticator, JWTAuthenticatorService, JWTAuthenticatorSettings }
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.impl.providers.oauth2.FacebookProvider

import com.mohiva.play.silhouette.impl.exceptions.OAuth2StateException
import com.mohiva.play.silhouette.impl.providers.OAuth2Provider._
import com.mohiva.play.silhouette.impl.providers.oauth2.state.{ CookieStateProvider, CookieStateSettings }
import com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieStateProvider._
import com.mohiva.play.silhouette.impl.providers.{ OAuth2State, OAuth2StateProvider }

import com.mohiva.play.silhouette.crypto.{ JcaCookieSigner, JcaCookieSignerSettings, JcaCrypter, JcaCrypterSettings }
import com.mohiva.play.silhouette.api.crypto.{ CookieSigner, Crypter }
import com.mohiva.play.silhouette.impl.providers.oauth1.secrets._
//import com.mohiva.play.silhouette.impl.providers.{CredentialsProvider, SocialProviderRegistry}
import com.mohiva.play.silhouette.impl.services.GravatarService
import com.mohiva.play.silhouette.impl.util.{ DefaultFingerprintGenerator, PlayCacheLayer, SecureRandomIDGenerator }
import com.mohiva.play.silhouette.password.BCryptPasswordHasher
import com.mohiva.play.silhouette.persistence.daos.{ DelegableAuthInfoDAO, InMemoryAuthInfoDAO }
import com.mohiva.play.silhouette.persistence.repositories.DelegableAuthInfoRepository
import daos._
import models.UserModel
import net.codingwell.scalaguice.ScalaModule
import play.api.Configuration
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.ws.WSClient
import services.AuthService
import utils.ErrorHandler
import utils.auth.JWTEnv

import scala.concurrent.duration.{ FiniteDuration, Duration }

class Module extends AbstractModule with ScalaModule {

  def configure() {
    bind[IdentityService[UserModel]].to[AuthService]
    bind[UserDao].to[MongoUserDao]
    bind[CoreoDao].to[MongoCoreoDao]
    bind[UserTokenDao].to[MongoUserTokenDao]
    bind[SecuredErrorHandler].to[ErrorHandler]
    bind[DelegableAuthInfoDAO[PasswordInfo]].to[PasswordInfoDao]

    bind[DelegableAuthInfoDAO[OAuth1Info]].to[OAuth1InfoDao]
    bind[DelegableAuthInfoDAO[OAuth2Info]].to[OAuth2InfoDao]
    //bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info]) //.to[OAuth2InfoDao]
    //bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo])

    bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
    bind[PasswordHasher].toInstance(new BCryptPasswordHasher)
    bind[EventBus].toInstance(EventBus())
    bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
    bind[Clock].toInstance(Clock())
    bind[CacheLayer].to[PlayCacheLayer]
    bind[Silhouette[JWTEnv]].to[SilhouetteProvider[JWTEnv]]
    bind[IdentityService[UserModel]].to[AuthService]
  }

  @Provides
  def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client)

  @Provides
  def provideEnvironment(
    identityService: IdentityService[UserModel],
    authenticatorService: AuthenticatorService[JWTAuthenticator],
    eventBus: EventBus): Environment[JWTEnv] = {

    Environment[JWTEnv](
      identityService,
      authenticatorService,
      Seq(),
      eventBus)
  }

  /**
   * Provides the Facebook provider.
   *
   * @param httpLayer The HTTP layer implementation.
   * @param stateProvider The OAuth2 state provider implementation.
   * @param configuration The Play configuration.
   * @return The Facebook provider.
   */
  @Provides
  def provideFacebookProvider(
    httpLayer: HTTPLayer,
    stateProvider: OAuth2StateProvider,
    configuration: Configuration): FacebookProvider = {

/*    val authUrl = configuration.getString("silhouette.facebook.authorizationURL") //.getOrElse("AuthUrl Not found")
    val acsTknUrl = configuration.getString("silhouette.facebook.accessTokenURL") //.getOrElse("AcsTknUrl Not found")
    val rdrctUrl = configuration.getString("silhouette.facebook.redirectURL") //.getOrElse("RdrctUrl Not found")
    val clientId = configuration.getString("silhouette.facebook.clientID") //.getOrElse("clientID Not found")
    val clientSecret = configuration.getString("silhouette.facebook.clientSecret") //.getOrElse("clientSecret Not found")
    val scope = configuration.getString("silhouette.facebook.scope") //.getOrElse("scope Not found")

    println("authUrl: " + authUrl)
    println("acsTknUrl: " + acsTknUrl)
    println("rdrctUrl: " + rdrctUrl)
    println("clientId: " + clientId)
    println("clientSecret: " + clientSecret)
    println("scope: " + scope)*/

    //val settings = new OAuth2Settings(authUrl, acsTknUrl.get, rdrctUrl.get, None, clientId.get, clientSecret.get, scope)

    val settings = OAuth2Settings(
      authorizationURL = configuration.getString("silhouette.facebook.authorizationURL"),
      accessTokenURL = configuration.getString("silhouette.facebook.accessTokenURL").get,
      redirectURL = configuration.getString("silhouette.facebook.redirectURL").get,
      clientID = configuration.getString("silhouette.facebook.clientID").get,
      clientSecret = configuration.getString("silhouette.facebook.clientSecret").get,
      scope = configuration.getString("silhouette.facebook.scope")
    )

    //println("settings: " + settings)

    //new FacebookProvider(httpLayer, stateProvider, configuration.underlying.as[OAuth2Settings]("silhouette.facebook"))
    new FacebookProvider(httpLayer, stateProvider, settings)
  }

  /**
   * Provides the cookie signer for the authenticator.
   *
   * @param configuration The Play configuration.
   * @return The cookie signer for the authenticator.
   */
  @Provides @Named("authenticator-cookie-signer")
  def provideAuthenticatorCookieSigner(configuration: Configuration): CookieSigner = {
    //val config = configuration.underlying.as[JcaCookieSignerSettings]("silhouette.authenticator.cookie.signer")
    val config = JcaCookieSignerSettings(key = configuration.getString("silhouette.authenticator.cookie.signer").get)

    new JcaCookieSigner(config)
  }

  /**
   * Provides the cookie signer for the OAuth1 token secret provider.
   *
   * @param configuration The Play configuration.
   * @return The cookie signer for the OAuth1 token secret provider.
   */
  @Provides @Named("oauth1-token-secret-cookie-signer")
  def provideOAuth1TokenSecretCookieSigner(configuration: Configuration): CookieSigner = {
    //val config = configuration.underlying.as[JcaCookieSignerSettings]("silhouette.oauth1TokenSecretProvider.cookie.signer")
    val config = JcaCookieSignerSettings(configuration.getString("silhouette.oauth1TokenSecretProvider.cookie.signer").get)

    new JcaCookieSigner(config)
  }

  /**
   * Provides the crypter for the OAuth1 token secret provider.
   *
   * @param configuration The Play configuration.
   * @return The crypter for the OAuth1 token secret provider.
   */
  @Provides @Named("oauth1-token-secret-crypter")
  def provideOAuth1TokenSecretCrypter(configuration: Configuration): Crypter = {
    //val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.oauth1TokenSecretProvider.crypter")
    val config = JcaCrypterSettings(key = configuration.getString("silhouette.oauth1TokenSecretProvider.crypter").get)

    new JcaCrypter(config)
  }


  /**
   * Provides the cookie signer for the OAuth2 state provider.
   *
   * @param configuration The Play configuration.
   * @return The cookie signer for the OAuth2 state provider.
   */
  @Provides @Named("oauth2-state-cookie-signer")
  def provideOAuth2StageCookieSigner(configuration: Configuration): CookieSigner = {
    //val config = configuration.underlying.as[JcaCookieSignerSettings]("silhouette.oauth2StateProvider.cookie.signer")
    
    val config = JcaCookieSignerSettings(key = configuration.getString("silhouette.oauth2StateProvider.cookie.signer").get)
    println("config: " + config)

    new JcaCookieSigner(config)
  }

  /**
   * Provides the OAuth1 token secret provider.
   *
   * @param cookieSigner The cookie signer implementation.
   * @param crypter The crypter implementation.
   * @param configuration The Play configuration.
   * @param clock The clock instance.
   * @return The OAuth1 token secret provider implementation.
   */
  @Provides
  def provideOAuth1TokenSecretProvider(
    @Named("oauth1-token-secret-cookie-signer") cookieSigner: CookieSigner,
    @Named("oauth1-token-secret-crypter") crypter: Crypter,
    configuration: Configuration,
    clock: Clock): OAuth1TokenSecretProvider = {

    //val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider")
    val settings = CookieSecretSettings(
      cookieName = configuration.getString("oauth1TokenSecretProvider.cookieName").get,
      cookiePath = configuration.getString("oauth1TokenSecretProvider.cookiePath").get,
      secureCookie = configuration.getString("oauth1TokenSecretProvider.secureCookie").get.toBoolean,
      httpOnlyCookie = configuration.getString("oauth1TokenSecretProvider.httpOnlyCookie").get.toBoolean,
      expirationTime = Duration.apply(configuration.getString("oauth1TokenSecretProvider.expirationTime").get).asInstanceOf[FiniteDuration] //.get.collect { case d: FiniteDuration => d }
      )
    new CookieSecretProvider(settings, cookieSigner, crypter, clock)
  }

  //Duration.apply(myString).asInstanceOf[FiniteDuration]

  /**
   * Provides the OAuth2 state provider.
   *
   * @param idGenerator The ID generator implementation.
   * @param cookieSigner The cookie signer implementation.
   * @param configuration The Play configuration.
   * @param clock The clock instance.
   * @return The OAuth2 state provider implementation.
   */
  @Provides
  def provideOAuth2StateProvider(
    idGenerator: IDGenerator,
    @Named("oauth2-state-cookie-signer") cookieSigner: CookieSigner,
    configuration: Configuration, clock: Clock): OAuth2StateProvider = {

    //val settings = configuration.underlying.as[CookieStateSettings]("silhouette.oauth2StateProvider")
    val settings = CookieStateSettings(
      cookieName = configuration.getString("silhouette.oauth2StateProvider.cookieName").get,
      cookiePath = configuration.getString("silhouette.oauth2StateProvider.cookiePath").get,
      secureCookie = configuration.getString("silhouette.oauth2StateProvider.secureCookie").get.toBoolean,
      httpOnlyCookie = configuration.getString("silhouette.oauth2StateProvider.httpOnlyCookie").get.toBoolean,
      expirationTime = Duration.apply(configuration.getString("silhouette.oauth2StateProvider.expirationTime").get).asInstanceOf[FiniteDuration] //.get.collect { case d: FiniteDuration => d }
      )
    new CookieStateProvider(settings, idGenerator, cookieSigner, clock)
  }

  /**
   * Provides the social provider registry.
   *
   * @param facebookProvider The Facebook provider implementation.
   * @param googleProvider The Google provider implementation.
   * @param vkProvider The VK provider implementation.
   * @param clefProvider The Clef provider implementation.
   * @param twitterProvider The Twitter provider implementation.
   * @param xingProvider The Xing provider implementation.
   * @param yahooProvider The Yahoo provider implementation.
   * @return The Silhouette environment.
   */
  @Provides
  def provideSocialProviderRegistry(
    facebookProvider: FacebookProvider //,
    //googleProvider: GoogleProvider,
    //twitterProvider: TwitterProvider,
    ): SocialProviderRegistry = {

    SocialProviderRegistry(Seq(
      //googleProvider,
      facebookProvider //,
      //twitterProvider,
      ))
  }

  @Provides
  def provideAuthenticatorService(
    fingerprintGenerator: FingerprintGenerator,
    idGenerator: IDGenerator,
    configuration: Configuration,
    clock: Clock): AuthenticatorService[JWTAuthenticator] = {

    val settings = JWTAuthenticatorSettings(
      sharedSecret = configuration.getString("play.crypto.secret").get)

    new JWTAuthenticatorService(
      settings = settings,
      repository = None,
      authenticatorEncoder = new Base64AuthenticatorEncoder,
      idGenerator = idGenerator,
      clock = Clock())
  }

  @Provides
  def provideAuthInfoRepository(
    passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo]): AuthInfoRepository = {

    new DelegableAuthInfoRepository(passwordInfoDAO)
  }

  @Provides
  def providePasswordHasherRegistry(passwordHasher: PasswordHasher): PasswordHasherRegistry = {
    new PasswordHasherRegistry(passwordHasher)
  }

  @Provides
  def provideCredentialsProvider(
    authInfoRepository: AuthInfoRepository,
    passwordHasherRegistry: PasswordHasherRegistry): CredentialsProvider = {

    new CredentialsProvider(authInfoRepository, passwordHasherRegistry)
  }

  @Provides
  def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer)

}

That’s the error I’m getting:

application - Unexpected provider error
com.mohiva.play.silhouette.impl.exceptions.OAuth2StateException: [Silhouette][CookieState] State cookie doesn't exists for name: OAuth2State
	at com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieStateProvider.clientState(CookieState.scala:194)
	at com.mohiva.play.silhouette.impl.providers.oauth2.state.CookieStateProvider.validate(CookieState.scala:150)
	at com.mohiva.play.silhouette.impl.providers.OAuth2Provider$class.authenticate(OAuth2Provider.scala:111)
	at com.mohiva.play.silhouette.impl.providers.oauth2.FacebookProvider.authenticate(FacebookProvider.scala:114)
	at controllers.SocialAuthController$$anonfun$authenticate$1.apply(SocialAuthController.scala:79)
	at controllers.SocialAuthController$$anonfun$authenticate$1.apply(SocialAuthController.scala:75)
	at play.api.mvc.Action$.invokeBlock(Action.scala:498)
	at play.api.mvc.Action$.invokeBlock(Action.scala:495)
	at play.api.mvc.ActionBuilder$$anon$2.apply(Action.scala:458)
	at play.api.mvc.Action$$anonfun$apply$2$$anonfun$apply$5$$anonfun$apply$6.apply(Action.scala:112)

and this is my SocialAuthController:

class SocialAuthController @Inject() (
  val messagesApi: MessagesApi,
  silhouette: Silhouette[JWTEnv],
  userService: AuthService,
  authInfoRepository: AuthInfoRepository,
  socialProviderRegistry: SocialProviderRegistry,
  cache: CacheApi)
  extends Controller with I18nSupport { //with Logger

  
  /**
   * Authenticates a user against a social provider.
   *
   * @param provider The ID of the provider to authenticate against.
   * @return The result to display.
   */
  def authenticate(provider: String) = Action.async { implicit request =>
    println("ma da qui dentro passo? " + provider)
    (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, Ok)
          } yield {
            silhouette.env.eventBus.publish(LoginEvent(user, request))
            //result
            Ok(Json.obj("token" -> value))
          }
        }
      case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
    }).recover {
      case e: ProviderException =>
        Logger.error("Unexpected provider error", e)
        Unauthorized(Json.obj("message" -> Messages("could.not.authenticate")))      
    }
  }
}

Thanks in advance,

Stefano Tondo


#2

Hi,

the OAuth2 provider can use a state cookie to protect the application against CSRF attacks. This is configured by default in the seed template because it’s a simple web application. You can use the DummyState implementation to omit the validation on the backend side. The Angular seed application uses also the JWT authenticator and it’s configured with the DummyState

Best regards,
Christian


#3

Thanks Christian, switching to DummyState solved the problem, but also had to modify SocialAuthController to make it work.

I’m going to paste it here, because I really don’t think that it should be working the way I implemented it, so maybe you or someone else can point me in the right direction, to make it work properly.

class SocialAuthController @Inject() (
  val messagesApi: MessagesApi,
  silhouette: Silhouette[JWTEnv],
  userService: AuthService,
  authInfoRepository: AuthInfoRepository,
  socialProviderRegistry: SocialProviderRegistry,
  cache: CacheApi,
  ws: WSClient,
  configuration: Configuration)
  extends Controller with I18nSupport {

  def authenticate(provider: String) = Action.async(parse.json) {
    implicit request =>
      provider match {
        case "facebook" =>
          val code = (request.body \ "code").as[String] 
          val clientId = (request.body \ "clientId").as[String] 
          val redirectUri = (request.body \ "redirectUri").as[String] 
          val clientSecret = configuration.getString("silhouette.facebook.clientSecret").get         

          val authInfo: Option[OAuth2Info] = Await.result(ws.url(s"https://graph.facebook.com/oauth/access_token?client_id=$clientId&redirect_uri=$redirectUri&client_secret=$clientSecret&code=$code")
          //.withQueryString("client_id" -> clientId, "redirect_uri" -> redirectUri, "client_secret" -> clientSecret, "code" -> code)
          
            .get().map { response =>
              println("response:" + response.body)
              Some(OAuth2Info(accessToken = response.body.substring(13), expiresIn = Some(300)))

          }, Duration.Inf)
          

          (socialProviderRegistry.get[FacebookProvider](provider) match {
            case Some(p: FacebookProvider) =>

              for {
                profile <- p.retrieveProfile(authInfo.get)
                user <- userService.save(profile)
                customClaims = Json.obj(
                  "id" -> user.id,                 
                  "isAdmin" -> user.isAdmin
                )
                authInfo <- authInfoRepository.save(profile.loginInfo, authInfo.get)
                authenticator <- silhouette.env.authenticatorService.create(profile.loginInfo).map(_.copy(customClaims=Some(customClaims)))
                token <- silhouette.env.authenticatorService.init(authenticator)
              } yield {
                silhouette.env.eventBus.publish(LoginEvent(user, request))
                Ok(Json.obj("token" -> token))
              }
            case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
          }).recover {
            case e: ProviderException =>
              Logger.error("Unexpected provider error", e)
              Unauthorized(Json.obj("message" -> Messages("could.not.authenticate")))
          }
        case _ =>
          Future(BadRequest(Json.obj(
            "message" -> "You can use only Facebook account for authentication.")))
      }
  }

At first I tried using the below authenticate function:

  def authenticate(provider: String) = Action.async { implicit request =>
    println("ma da qui dentro passo? " + provider)
    (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, Ok)
          } yield {
            silhouette.env.eventBus.publish(LoginEvent(user, request))
            //result
            Ok(Json.obj("token" -> value))
          }
        }
      case _ => Future.failed(new ProviderException(s"Cannot authenticate with unexpected social provider $provider"))
    }).recover {
      case e: ProviderException =>
        Logger.error("Unexpected provider error", e)
        Unauthorized(Json.obj("message" -> Messages("could.not.authenticate")))
    }
  }

but it didn’t work. Debugging it, I came up with the solution above, in which I make explicitly the server to server call to the Facebook Graph OAuth API, so I can instantiate correctly the OAuth2Info case class. Again, I don’t think it should work like that.

On the client side I’m using Angular/Satellizer and since I was able to get it to work with the above implementation, I think everything is set up correctly.

Please, can you or anyone else tell me what’s wrong and how it should be done?

Thanks again in advance.

Best regards,

Stefano


#4

Hi,

I’m not really sure what you try to achieve? Have you looked at the Angular Seed template? It uses also Satellizer to handle the client side authentication.

Best regards,
Christian