BearerToken Authentication


#1

Well, I am stuck completely, please help

I want to authorize my api via bearer token, which I have

package controllers

import com.mohiva.play.silhouette.api.actions.SecuredRequest
import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository
import com.mohiva.play.silhouette.api.{Logger, Silhouette}
import javax.inject.Inject
import models.services.UserService
import play.api.i18n.I18nSupport
import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request}
import providers.CustomProvider
import utils.auth.{BTEnv, WithProvider}

import scala.concurrent.{ExecutionContext, Future}

class RestController @Inject()(
                                components: ControllerComponents,
                                silhouette: Silhouette[BTEnv],
                                userService: UserService,
                                authInfoRepository: AuthInfoRepository,
                                customProvider: CustomProvider
                                //socialProviderRegistry: SocialProviderRegistry
                              )(
                                implicit
                                ex: ExecutionContext
                              ) extends AbstractController(components) with I18nSupport with Logger {
  
  def secured = silhouette.SecuredAction(WithProvider(CustomProvider.ID)) { implicit request: SecuredRequest[BTEnv, AnyContent] =>
    
    Ok("42")
  }
  
  def unsecured = silhouette.UnsecuredAction.async {
    implicit request: Request[AnyContent] => Future.successful(Ok("420"))
  }
  
  
}

custom oauth2provider:

package providers

import com.mohiva.play.silhouette.api.LoginInfo
import com.mohiva.play.silhouette.api.util.{ ExtractableRequest, HTTPLayer }
import com.mohiva.play.silhouette.impl.exceptions.ProfileRetrievalException
import com.mohiva.play.silhouette.impl.providers._
import play.api.libs.json.{ Format, JsObject, JsValue }
import play.api.mvc.Result
import providers.CustomProvider._
import scala.concurrent.Future
import scala.reflect.ClassTag

trait BaseCustomProvider extends OAuth2Provider {
  override type Content = JsValue

  override protected val urls = Map("api" -> settings.apiURL.getOrElse(API))
  override val id = ID
  override def authenticate[B]()(implicit request: ExtractableRequest[B]): Future[Either[Result, OAuth2Info]] =
    {
      super.authenticate()
    }

  override protected def handleAuthorizationFlow[B](stateHandler: SocialStateHandler)(implicit request: ExtractableRequest[B]) = {
    super.handleAuthorizationFlow(stateHandler)
  }

  override def authenticate[S <: SocialStateItem, B](userState: S)(implicit format: Format[S], request: ExtractableRequest[B], classTag: ClassTag[S]): Future[Either[Result, StatefulAuthInfo[OAuth2Info, S]]] = {
    super.authenticate(userState)
  }

  override 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, authInfo)
      }
    }
  }

}

class CustomProfileParser extends SocialProfileParser[JsValue, CommonSocialProfile, OAuth2Info] {

  /**
   * Parses the social profile.
   *
   * @param json     The content returned from the provider.
   * @param authInfo The auth info to query the provider again for additional data.
   * @return The social profile from given result.
   */
  override def parse(json: JsValue, authInfo: OAuth2Info) = 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)
  }
}

class CustomProvider(
  protected val httpLayer: HTTPLayer,
  protected val stateHandler: SocialStateHandler,
  val settings: OAuth2Settings)
  extends BaseCustomProvider with CommonSocialProfileBuilder {
  override type Self = CustomProvider
  override val profileParser = new CustomProfileParser;

  override def authenticate[S <: SocialStateItem, B](userState: S)(implicit format: Format[S], request: ExtractableRequest[B], classTag: ClassTag[S]): Future[Either[Result, StatefulAuthInfo[OAuth2Info, S]]] =
    {
      super.authenticate(userState)
    }

  override def authenticate[B]()(implicit request: ExtractableRequest[B]): Future[Either[Result, OAuth2Info]] = {
    super.authenticate()
  }

  override def withSettings(f: (Settings) => Settings) = new CustomProvider(httpLayer, stateHandler, f(settings))
}

object CustomProvider {
  val ID = "custom"
  val API = "http://url.here/oauth/token"
  val SpecifiedProfileError = "[Silhouette][%s] Error retrieving profile information. Error code: %s, message: %s"
}

SilhouetteModule

package modules

import com.google.inject.name.Named
import com.google.inject.{AbstractModule, Provides}
import com.mohiva.play.silhouette.api.actions.{SecuredErrorHandler, UnsecuredErrorHandler}
import com.mohiva.play.silhouette.api.crypto._
import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository
import com.mohiva.play.silhouette.api.services._
import com.mohiva.play.silhouette.api.util._
import com.mohiva.play.silhouette.api.{Environment, EventBus, Silhouette, SilhouetteProvider}
import com.mohiva.play.silhouette.crypto.{JcaCrypter, JcaCrypterSettings, JcaSigner, JcaSignerSettings}
import com.mohiva.play.silhouette.impl.authenticators._
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.impl.providers.oauth1.secrets.{CookieSecretProvider, CookieSecretSettings}
import com.mohiva.play.silhouette.impl.providers.oauth2._
import com.mohiva.play.silhouette.impl.providers.state.{CsrfStateItemHandler, CsrfStateSettings}
import com.mohiva.play.silhouette.impl.services._
import com.mohiva.play.silhouette.impl.util._
import com.mohiva.play.silhouette.password.{BCryptPasswordHasher, BCryptSha256PasswordHasher}
import com.mohiva.play.silhouette.persistence.daos.{DelegableAuthInfoDAO, InMemoryAuthInfoDAO}
import com.mohiva.play.silhouette.persistence.repositories.{CacheAuthenticatorRepository, DelegableAuthInfoRepository}
import com.typesafe.config.Config
import models.daos._
import models.services.{UserService, UserServiceImpl}
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.ceedubs.ficus.readers.ValueReader
import net.codingwell.scalaguice.ScalaModule
import play.api.Configuration
import play.api.libs.ws.WSClient
import play.api.mvc.{Cookie}
import providers.{CustomProvider}
import utils.auth.{BTEnv, CustomSecuredErrorHandler, CustomUnsecuredErrorHandler}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.FiniteDuration

/**
 * The Guice module which wires all Silhouette dependencies.
 */
class SilhouetteModule extends AbstractModule with ScalaModule {

  /**
   * A very nested optional reader, to support these cases:
   * Not set, set None, will use default ('Lax')
   * Set to null, set Some(None), will use 'No Restriction'
   * Set to a string value try to match, Some(Option(string))
   */
  implicit val sameSiteReader: ValueReader[Option[Option[Cookie.SameSite]]] =
    (config: Config, path: String) => {
      if (config.hasPathOrNull(path)) {
        if (config.getIsNull(path))
          Some(None)
        else {
          Some(Cookie.SameSite.parse(config.getString(path)))
        }
      } else {
        None
      }
    }

  /**
   * Configures the module.
   */
  def configure() {
    bind[Silhouette[BTEnv]].to[SilhouetteProvider[BTEnv]]
    bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler]
    bind[SecuredErrorHandler].to[CustomSecuredErrorHandler]
    bind[UserService].to[UserServiceImpl]
    bind[UserDAO].to[UserDAOImpl]
   // bind(classOf[CacheLayer]).to(classOf[PlayCacheLayer])
    bind[CacheLayer].to[PlayCacheLayer]
    bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
  //  bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
    bind[EventBus].toInstance(EventBus())
    bind[Clock].toInstance(Clock())

    // Replace this with the bindings to your concrete DAOs
    bind[DelegableAuthInfoDAO[PasswordInfo]].toInstance(new InMemoryAuthInfoDAO[PasswordInfo])
    bind[DelegableAuthInfoDAO[OAuth1Info]].toInstance(new InMemoryAuthInfoDAO[OAuth1Info])
    bind[DelegableAuthInfoDAO[OAuth2Info]].toInstance(new InMemoryAuthInfoDAO[OAuth2Info])
    bind[DelegableAuthInfoDAO[OpenIDInfo]].toInstance(new InMemoryAuthInfoDAO[OpenIDInfo])
  }

  /**
   * Provides the HTTP layer implementation.
   *
   * @param client Play's WS client.
   * @return The HTTP layer implementation.
   */
  @Provides
  def provideHTTPLayer(client: WSClient): HTTPLayer = new PlayHTTPLayer(client)

  /**
   * Provides the Silhouette environment.
   *
   * @param userService The user service implementation.
   * @param authenticatorService The authentication service implementation.
   * @param eventBus The event bus instance.
   * @return The Silhouette environment.
   */
  @Provides
  def provideEnvironment(
    userService: UserService,
    authenticatorService: AuthenticatorService[BearerTokenAuthenticator],
    eventBus: EventBus): Environment[BTEnv] = {

    Environment[BTEnv](
      userService,
      authenticatorService,
      Seq(),
      eventBus
    )
  }

  /**
   * Provides the social provider registry.
   *
   * @param vkProvider The VK provider implementation.
   *
   * @return The Silhouette environment.
   */
  @Provides
  def provideSocialProviderRegistry(
                                     customProvider: CustomProvider,
   ): SocialProviderRegistry = {

    SocialProviderRegistry(Seq(
      customProvider,
    ))
  }

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

    new JcaSigner(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")

    new JcaCrypter(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)
  }

  /**
   * 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 authenticator.
   *
   * @param configuration The Play configuration.
   * @return The signer for the authenticator.
   */
  @Provides @Named("authenticator-signer")
  def provideAuthenticatorSigner(configuration: Configuration): Signer = {
    val config = configuration.underlying.as[JcaSignerSettings]("silhouette.authenticator.signer")

    new JcaSigner(config)
  }

  /**
   * Provides the crypter for the authenticator.
   *
   * @param configuration The Play configuration.
   * @return The crypter for the authenticator.
   */
  @Provides @Named("authenticator-crypter")
  def provideAuthenticatorCrypter(configuration: Configuration): Crypter = {
    val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.authenticator.crypter")

    new JcaCrypter(config)
  }

  /**
   * Provides the auth info repository.
   *
   * @param passwordInfoDAO The implementation of the delegable password auth info DAO.
   * @param oauth1InfoDAO The implementation of the delegable OAuth1 auth info DAO.
   * @param oauth2InfoDAO The implementation of the delegable OAuth2 auth info DAO.
   * @param openIDInfoDAO The implementation of the delegable OpenID auth info DAO.
   * @return The auth info repository instance.
   */
  @Provides
  def provideAuthInfoRepository(
    passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo],
    oauth1InfoDAO: DelegableAuthInfoDAO[OAuth1Info],
    oauth2InfoDAO: DelegableAuthInfoDAO[OAuth2Info],
    openIDInfoDAO: DelegableAuthInfoDAO[OpenIDInfo]): AuthInfoRepository = {

    new DelegableAuthInfoRepository(passwordInfoDAO, oauth1InfoDAO, oauth2InfoDAO, openIDInfoDAO)
  }

  /**
   * Provides the authenticator service.
   *
   * @param signer The signer implementation.
   * @param crypter The crypter implementation.
   * @param cookieHeaderEncoding Logic for encoding and decoding `Cookie` and `Set-Cookie` headers.
   * @param fingerprintGenerator The fingerprint generator implementation.
   * @param idGenerator The ID generator implementation.
   * @param configuration The Play configuration.
   * @param clock The clock instance.
   * @return The authenticator service.
   */
//  @Provides
//  def provideAuthenticatorService(
//    @Named("authenticator-signer") signer: Signer,
//    @Named("authenticator-crypter") crypter: Crypter,
//    cookieHeaderEncoding: CookieHeaderEncoding,
//    fingerprintGenerator: FingerprintGenerator,
//    idGenerator: IDGenerator,
//    configuration: Configuration,
//    clock: Clock): AuthenticatorService[CookieAuthenticator] = {
//
//    val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator")
//    val authenticatorEncoder = new CrypterAuthenticatorEncoder(crypter)
//
//    new CookieAuthenticatorService(config, None, signer, cookieHeaderEncoding, authenticatorEncoder, fingerprintGenerator, idGenerator, clock)
//  }

 
  
  @Provides
  def provideAuthenticatorService(
                                   idGenerator : IDGenerator,
                                   clock : Clock,
                                   cacheLayer : CacheLayer
                                 ) : AuthenticatorService[BearerTokenAuthenticator] = {
    val settings:BearerTokenAuthenticatorSettings = BearerTokenAuthenticatorSettings(
    fieldName = "X-Auth-Token",
    requestParts = Some(Seq(RequestPart.Headers)),
    authenticatorIdleTimeout = None,
    authenticatorExpiry = FiniteDuration(1000, "hours")
    )
  //  val zsx = configuration.underlying.
    val authenticatorRepository = new CacheAuthenticatorRepository[BearerTokenAuthenticator](cacheLayer)
    new BearerTokenAuthenticatorService(settings, authenticatorRepository, idGenerator, clock)
  }
  /**
   * Provides the avatar service.
   *
   * @param httpLayer The HTTP layer implementation.
   * @return The avatar service implementation.
   */
  @Provides
  def provideAvatarService(httpLayer: HTTPLayer): AvatarService = new GravatarService(httpLayer)

  /**
   * Provides the OAuth1 token secret provider.
   *
   * @param signer The 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-signer") signer: Signer,
    @Named("oauth1-token-secret-crypter") crypter: Crypter,
    configuration: Configuration,
    clock: Clock): OAuth1TokenSecretProvider = {

    val settings = configuration.underlying.as[CookieSecretSettings]("silhouette.oauth1TokenSecretProvider")
    new CookieSecretProvider(settings, signer, crypter, clock)
  }

  /**
   * 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)
  }

  /**
   * 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)
  }

  /**
   * Provides the password hasher registry.
   *
   * @return The password hasher registry.
   */
  @Provides
  def providePasswordHasherRegistry(): PasswordHasherRegistry = {
    PasswordHasherRegistry(new BCryptSha256PasswordHasher(), Seq(new BCryptPasswordHasher()))
  }

 
  
  @Provides
  def providesCustomProvider(httpLayer: HTTPLayer,
                             socialStateHandler: SocialStateHandler,
                             configuration: Configuration):CustomProvider = {
    new CustomProvider(httpLayer, socialStateHandler, configuration.underlying.as[OAuth2Settings]("silhouette.custom"))
  }
  
}

authenticate method in provider do not run (


#2

Where do you cal the authenticate method of your provider? It gets not called automatically.


#3

Maybe you misunderstand the example code:

silhouette.SecuredAction(WithProvider(CustomProvider.ID))

Doesn’t call your provider. It uses an authorization rule called WithProvider that grants only access if a user has authenticated with the given provider.


#4

Thank you for your answer!
But, what is the recommended way to authorize api call with access_token provided?
(authorization: Bearer {token here})

def securedAction = silhouette.UnsecuredAction {
provider.authenticate(…)
… and so on?}

documentation, as I can see do not cover such scenario


#5

#6

So, if I have controller with action that I want to protect that way, I should perform two steps:

controller with action to authorize:

class SomeController {
def protectedAction = silhouette.SecuredAction(WithProvider(“myProvider”)) {…}

authorize controller:
class SocialAuthController {
def authenticate(provider: String) = Action.async {…}

and

  1. sent request to authenticate method with token provided
  2. sent another request to protectedAction?

Sorry, but this approach looks weird for me, hope that I do not understand you well


#7

With a secured action you protect an endpoint. If a user try to access such a secured endpoint without an authenticator, then Silhouette calls either a global or a local error handler. In this error handler you typically redirect the user to a login page. On this login page the user can either login with a form or it can use a social login (OAuth1, OAuth2, …). If you use OAuth2 as in your case, then Silhouette starts the OAuth2 flow for you by redirecting to your OAuth2 provider. This OAuth2 provider asks for your credentials and redirects back to the app. The SocialAuthController from my last post does exactly handle this Oauth2 flow.

I have the gut feeling, that you need something different. Maybe you could describe your use case a bit more detailed? Could you please provide a flow chart?


#8

I will try:
I perform request to oauth2 server, sending clientid, secret as well as login and password
(via curl, or with Postman application)
oauth2 server response to me with access_token and refresh_token

than, I perform another request, this time - to play+silhouette based api
and this time, I send request header of "Authorization: Bearer {access_token value here}
call to api can be performed via curl, or Postman as well


#9

You will use Play as a resource server?

OK, this is completely different was Silhouette provides for OAuth2. You can implement such functionality with a RequestProvider.


#10

Exactly!
Play is resource server in my case.
So I must implement my own RequestProvider, and not Oauth2Provider in this case?

and manually send token extracted from request to token validation endpoint of oauth2 server?


#11

Yes, exactly!!! :+1:


#12

Will try this )
Thank you - can I ask you again in case of trouble?


#13

Sure, ask if you have problems


#14

Well, I’m stuck.

If I want to extract auth token from request header - should I extend RequestProvider with Oauth2Provider, and
use getTokenInfo from it?


#15

I was able to implement RequestProvider, and it’s authenticate method correctly returns LoginInfo or None, as mentioned in docs
but, silhouette.SecuredAction always fall into override def onNotAuthenticated(implicit request: RequestHeader)
in CustomSecuredErrorHandler

is this correct?
how to avoid such behavior?


#16

No, I wouldn’t do that. You can copy the code if needed. But the trait needs a lot of additional dependencies, that are not needed in your case.

but, silhouette.SecuredAction always fall into override def onNotAuthenticated(implicit request: RequestHeader)

What do you exactly mean? Could you provide an example code snippet?

As a side note. You should try to express your technical questions a bit more detailed. Please take a little more time to formulate your questions. That cost the responder less time, because he doesn’t have to ask what you mean.


#17

but, silhouette.SecuredAction always fall into override def onNotAuthenticated(implicit request: RequestHeader)

What do you exactly mean? Could you provide an example code snippet?

First of all, I’m basing on play-silhouette-seed project (https://github.com/mohiva/play-silhouette-seed)

I mean, that If I use SecuredAction, that even if my RequestProvider correctly wires into workflow, and it’s authenticate method correctly returns LoginInfo value,

CustomSecuredErrorHandler.onNotAuthenticated being called and unwanted redirect occurs.

I was able to solve the initial problem
“Protect play application (which is ResourceServer in this case), with token granted from separate IdentityServer” by using custom Action and using this action as Action.andThen(myAction)
so silhouette becomes unnecessary in my case
But I’d like to know, how to properly solve this problem with use of silhouette


#18

Please could you share the project on GitHub? With the RequestProvider and the Guice binding for the request provider