Implementing Backing Store (Reactive Mongo) for Play Silhouette JWT Authenticator


#1

I am currently building a REST Api using Play 2.5 (Scala) using Play Silhouette 4.0 as my authentication library.

I have a need to invalidate JWT per user therefore I will need to persist the authenticator in the database (mongodb). I am using reactive mongo 0.12.1.

Following the code examples of Silhouette’s CacheAuthenticatorRepository and MongoAuthInfoDAO for reactive mongo, I have created an authenticator repository just for the JWT Authenticator like so:

 package modules

    import javax.inject.Inject

    import com.mohiva.play.silhouette.api.StorableAuthenticator
    import com.mohiva.play.silhouette.api.repositories.AuthenticatorRepository
    import modules._
    import com.mohiva.play.silhouette.api.util._

    import scala.concurrent.Future
    import scala.concurrent.duration.Duration
    import scala.reflect.ClassTag

    import com.mohiva.play.silhouette.api.{ AuthInfo, LoginInfo }
    import com.mohiva.play.silhouette.persistence.exceptions.MongoException
    import play.api.Configuration
    import play.api.libs.concurrent.Execution.Implicits._
    import play.api.libs.json.{ Format, JsObject, Json }
    import play.modules.reactivemongo.ReactiveMongoApi
    import play.modules.reactivemongo.json._
    import reactivemongo.api.commands.WriteResult
    import reactivemongo.play.json.collection.JSONCollection

    import play.api.libs.json.Reads

    /**
     * Implementation of the authenticator repository which uses the reactive mongo to persist the authenticator.
     *
     * @param cacheLayer The cache layer implementation.
     * @tparam T The type of the authenticator to store.
     */
    class MongoAuthenticatorRepository[JWTAuthenticator] @Inject() (reactiveMongoApi: ReactiveMongoApi)
      extends AuthenticatorRepository[JWTAuthenticator] {

      def collection: Future[JSONCollection] = reactiveMongoApi.database.map(_.collection("jwt.auth.repo"))

      /**
       * Finds the authenticator for the given ID.
       *
       * @param id The authenticator ID.
       * @return The found authenticator or None if no authenticator could be found for the given ID.
       */
      override def find(id: String): Future[Option[JWTAuthenticator]] = {
        //cacheLayer.find[JWTAuthenticator](id)
        val query = Json.obj("id" -> id)
        collection.flatMap(_.find(query).one[JWTAuthenticator])
      }

      /**
       * Adds a new authenticator.
       *
       * @param authenticator JWTAuthenticatorhe authenticator to add.
       * @return JWTAuthenticatorhe added authenticator.
       */
      override def add(authenticator: JWTAuthenticator): Future[JWTAuthenticator] = {
        //cacheLayer.save[JWTAuthenticator](authenticator.id, authenticator, Duration.Inf)
        val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
        collection.flatMap(_.insert(obj))
      }

      /**
       * Updates an already existing authenticator.
       *
       * @param authenticator JWTAuthenticatorhe authenticator to update.
       * @return JWTAuthenticatorhe updated authenticator.
       */
      override def update(authenticator: JWTAuthenticator): Future[JWTAuthenticator] = {
        //cacheLayer.save[JWTAuthenticator](authenticator.id, authenticator, Duration.Inf)
        val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
        collection.flatMap(_.update(Json.obj("id" -> authenticator.id), obj, upsert = true))
      }

      /**
       * Removes the authenticator for the given ID.
       *
       * @param id JWTAuthenticatorhe authenticator ID.
       * @return An empty future.
       */
      override def remove(id: String): Future[Unit] = {
        //cacheLayer.remove(id)
        val query = Json.obj("id" -> id)
        collection.flatMap(_.remove(query))
      }
    }

I then implemented it by modifying my silhouette.module file like so:

@Provides def provideAuthenticatorService(authenticatorEncoder: AuthenticatorEncoder, idGenerator: IDGenerator, configuration: Configuration, clock: Clock): AuthenticatorService[JWTAuthenticator] = {
          val config = new JWTAuthenticatorSettings(
            fieldName = configuration.underlying.getString("silhouette.authenticator.fieldName"),
            //requestParts = Some(configuration.underlying.getAs[Seq[RequestPart.Value]]("silhouette.authenticator.requestParts").get),
            issuerClaim = configuration.underlying.getString("silhouette.authenticator.issuerClaim"),
            sharedSecret = configuration.underlying.getString("silhouette.authenticator.sharedSecret"),
            authenticatorExpiry = configuration.underlying.getAs[FiniteDuration]("silhouette.authenticator.authenticatorExpiry").get,
            authenticatorIdleTimeout = Some(configuration.underlying.getAs[FiniteDuration]("silhouette.authenticator.authenticatorIdleTimeout").get)
          )
          implicit lazy val format = Json.format[JWTAuthenticator]
          val repo = new MongoAuthenticatorRepository[JWTAuthenticator]
          new JWTAuthenticatorService(config, repo, authenticatorEncoder, idGenerator, clock)
        }

I keep having problems compiling the code and the errors shown are:

“No Json deserializer found for type JWTAuthenticator. Try to implement an implicit Reads or Format for this type.”

and

“Imported `MongoAuthenticatorRepository’ is permanently hidden by definition of class MongoAuthenticatorRepository in package modules”

I tried to implement a custom JWTAuthenticator class as well and setting implicit formats within the companion object. However the errors still persists.

I am new to Scala as well as Play Framework, would appreciate any pointers or tips on resolving the above issue! Thanks!!


#2

The first error comes from the fact that the implicit format isn’t in scope. You must define it in your MongoAuthenticatorRepository class and not in the Guice module.

class MongoAuthenticatorRepository[JWTAuthenticator] @Inject() (reactiveMongoApi: ReactiveMongoApi)
  extends AuthenticatorRepository[JWTAuthenticator] {

  implicit lazy val format = Json.format[JWTAuthenticator]

  ...
}

I think the second error comes from ‘import modules._’, because you import a class MongoAuthenticatorRepository which you define in the file.


#3

Hi akkie, thank you so much for your feedback! I have actually tried placing the implicit format in the MongoAuthenticatorRepository class but the same errors occur.

As for the second error I tried importing with ‘import modules.MongoAuthenticatorRepository’ and ‘import modules.{MongoAuthenticatorRepository}’ but both produces the same “Imported `MongoAuthenticatorRepository’ is permanently hidden by definition of class MongoAuthenticatorRepository in package modules” error.

I looked through the code for both SessionAuthenticator and CookieAuthenticator in the Silhouette codes and found these formats declared:

/**
 * The companion object of the authenticator.
 */
object CookieAuthenticator extends Logger {
  import com.mohiva.play.silhouette.api.util.JsonFormats._
  import play.api.libs.json.JodaReads._
  import play.api.libs.json.JodaWrites._

  /**
   * Converts the CookieAuthenticator to Json and vice versa.
   */
  implicit val jsonFormat = Json.format[CookieAuthenticator]

I also tried creating a custom JWTAuthenticator class and overriding it with the above, but it doesn’t seem to work.


#4

Hi akkie,

I have modified my code like so:

MongoAuthenticatorRepository.scala

package modules

import javax.inject.Inject

import com.mohiva.play.silhouette.api.StorableAuthenticator
import com.mohiva.play.silhouette.api.repositories.AuthenticatorRepository
import com.mohiva.play.silhouette.api.util._

import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.reflect.ClassTag

import com.mohiva.play.silhouette.api.{ AuthInfo, LoginInfo }
import com.mohiva.play.silhouette.persistence.exceptions.MongoException
import play.api.Configuration
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.{ Format, JsObject, Json }
import play.modules.reactivemongo.ReactiveMongoApi
import play.modules.reactivemongo.json._
import reactivemongo.api.commands.WriteResult
import reactivemongo.play.json.collection.JSONCollection

import play.api.libs.json.Reads


class MongoAuthenticatorRepository[JWTAuthenticator] @Inject() (reactiveMongoApi: ReactiveMongoApi)
  extends AuthenticatorRepository[JWTAuthenticator] {

  implicit lazy val format = Json.format[JWTAuthenticator]

  def collection: Future[JSONCollection] = reactiveMongoApi.database.map(_.collection("jwt.auth.repo"))


  override def find(id: String): Future[Option[JWTAuthenticator]] = {
    val query = Json.obj("id" -> id)
    collection.flatMap(_.find(query).one[JWTAuthenticator])
  }

  override def add(authenticator: JWTAuthenticator): Future[JWTAuthenticator] = {
    val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
    collection.flatMap(_.insert(obj))
  }

  override def update(authenticator: JWTAuthenticator): Future[JWTAuthenticator] = {
    val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
    collection.flatMap(_.update(Json.obj("id" -> authenticator.id), obj, upsert = true))
  }

  override def remove(id: String): Future[Unit] = {
    val query = Json.obj("id" -> id)
    collection.flatMap(_.remove(query))
  }
}

Silhouette.module

package modules

import scala.concurrent.duration._

import play.api.Configuration
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json._
import play.modules.reactivemongo.ReactiveMongoApi

import com.google.inject.{ AbstractModule, Provides }
import com.mohiva.play.silhouette.api.Env
import com.mohiva.play.silhouette.api.{ Environment, EventBus, Silhouette, SilhouetteProvider }
import com.mohiva.play.silhouette.api.actions.{ SecuredErrorHandler, UnsecuredErrorHandler }
import com.mohiva.play.silhouette.api.crypto.{AuthenticatorEncoder, Base64AuthenticatorEncoder}
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.impl.authenticators._
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.impl.services._
import com.mohiva.play.silhouette.impl.util._
import com.mohiva.play.silhouette.password.BCryptPasswordHasher
import com.mohiva.play.silhouette.persistence.daos.{ DelegableAuthInfoDAO, MongoAuthInfoDAO }
import com.mohiva.play.silhouette.persistence.repositories.DelegableAuthInfoRepository
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.ArbitraryTypeReader._
import net.codingwell.scalaguice.ScalaModule

import models.User
import models.services.UserService
import utils.auth.{ CustomSecuredErrorHandler, CustomUnsecuredErrorHandler }

import modules.{ MongoAuthenticatorRepository }

/**
 * The default env.
 */
trait JWTEnv extends Env {
  type I = User
  type A = JWTAuthenticator
}


/**
 * The Guice module which wires all Silhouette dependencies.
 */
class SilhouetteModule extends AbstractModule with ScalaModule {
  
  /**
   * Configures the module.
   */
  def configure(): Unit = {
    bind[Silhouette[JWTEnv]].to[SilhouetteProvider[JWTEnv]]
    bind[UnsecuredErrorHandler].to[CustomUnsecuredErrorHandler]
    bind[SecuredErrorHandler].to[CustomSecuredErrorHandler]
    bind[CacheLayer].to[PlayCacheLayer]
    bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
    bind[PasswordHasher].toInstance(new BCryptPasswordHasher)
    bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
    bind[AuthenticatorEncoder].toInstance(new Base64AuthenticatorEncoder)
    bind[EventBus].toInstance(EventBus())
    bind[Clock].toInstance(Clock())
  }

  /**
   * 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[JWTAuthenticator], eventBus: EventBus): 
    Environment[JWTEnv] = {
      Environment[JWTEnv](userService, authenticatorService, Seq(), eventBus)
    }

  /**
   * Provides the implementation of the delegable password auth info DAO.
   *
   * @param reactiveMongoApi The ReactiveMongo API.
   * @param config The Play configuration.
   * @return The implementation of the delegable password auth info DAO.
   */
  @Provides def providePasswordInfoDAO(reactiveMongoApi: ReactiveMongoApi, config: Configuration): DelegableAuthInfoDAO[PasswordInfo] = {
    implicit lazy val format = Json.format[PasswordInfo]
    new MongoAuthInfoDAO[PasswordInfo](reactiveMongoApi, config)
  }

  /**
   * Provides the authenticator service.
   *
   * @param authenticatorEncoder The authenticator encoder implementation.
   * @param idGenerator The ID generator implementation.
   * @param configuration The Play configuration.
   * @param clock The clock instance.
   * @return The authenticator service.
   */
  @Provides def provideAuthenticatorService(reactiveMongoApi: ReactiveMongoApi, authenticatorEncoder: AuthenticatorEncoder, idGenerator: IDGenerator, configuration: Configuration, clock: Clock): AuthenticatorService[JWTAuthenticator] = {
      val config = new JWTAuthenticatorSettings(
        fieldName = configuration.underlying.getString("silhouette.authenticator.fieldName"),
        //requestParts = Some(configuration.underlying.getAs[Seq[RequestPart.Value]]("silhouette.authenticator.requestParts").get),
        issuerClaim = configuration.underlying.getString("silhouette.authenticator.issuerClaim"),
        sharedSecret = configuration.underlying.getString("silhouette.authenticator.sharedSecret"),
        authenticatorExpiry = configuration.underlying.getAs[FiniteDuration]("silhouette.authenticator.authenticatorExpiry").get,
        authenticatorIdleTimeout = Some(configuration.underlying.getAs[FiniteDuration]("silhouette.authenticator.authenticatorIdleTimeout").get)
      )

      val repo = new MongoAuthenticatorRepository[JWTAuthenticator](reactiveMongoApi)
      new JWTAuthenticatorService(config, Some(repo), authenticatorEncoder, idGenerator, clock)
    }

  /**
   * Provides the password hasher registry.
   *
   * @param passwordHasher The default password hasher implementation.
   * @return The password hasher registry.
   */
  @Provides def providePasswordHasherRegistry(passwordHasher: PasswordHasher): PasswordHasherRegistry = {
    new PasswordHasherRegistry(passwordHasher)
  }

  /**
   * Provides the credentials provider.
   *
   * @param authInfoRepository The auth info repository implementation.
   * @param passwordHasherRegistry The password hasher registry.
   * @return The credentials provider.
   */
  @Provides def provideCredentialsProvider(authInfoRepository: AuthInfoRepository, passwordHasherRegistry: PasswordHasherRegistry): 
    CredentialsProvider = {
      new CredentialsProvider(authInfoRepository, passwordHasherRegistry)
    }

  /**
   * Provides the auth info repository.
   *
   * @param passwordInfoDAO The implementation of the delegable password auth info DAO.
   * @return The auth info repository instance.
   */
  @Provides def provideAuthInfoRepository(passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo]): 
    AuthInfoRepository = {
      new DelegableAuthInfoRepository(passwordInfoDAO)
    }

}

Here are the errors produced during compile:

[info] Compiling 2 Scala sources to \target\scala-2.11\classes...
[error] app\modules\MongoAuthenticatorRepository.scala:29: No unapply or unapplySeq function found
[error]   implicit lazy val format = Json.format[JWTAuthenticator]
[error]                                         ^
[error] app\modules\MongoAuthenticatorRepository.scala:36: No Json deserializer found for type JWTAuthenticator. Try to implement an implicit Reads or Format for this type.
[error]     collection.flatMap(_.find(query).one[JWTAuthenticator])
[error]                                         ^
[error] app\modules\MongoAuthenticatorRepository.scala:40: value id is not a member of type parameter JWTAuthenticator
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                              ^
[error] app\modules\MongoAuthenticatorRepository.scala:40: type mismatch;
[error]  found   : JWTAuthenticator
[error]  required: play.api.libs.json.Json.JsValueWrapper
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                                                      ^
[error] app\modules\MongoAuthenticatorRepository.scala:40: type mismatch;
[error]  found   : scala.concurrent.duration.Duration.Infinite
[error]  required: play.api.libs.json.Json.JsValueWrapper
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                                                                                            ^

[error] app\modules\MongoAuthenticatorRepository.scala:45: value id is not a member of type parameter JWTAuthenticator
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                              ^
[error] app\modules\MongoAuthenticatorRepository.scala:45: type mismatch;
[error]  found   : JWTAuthenticator
[error]  required: play.api.libs.json.Json.JsValueWrapper
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                                                      ^
[error] app\modules\MongoAuthenticatorRepository.scala:45: type mismatch;
[error]  found   : scala.concurrent.duration.Duration.Infinite
[error]  required: play.api.libs.json.Json.JsValueWrapper
[error]     val obj = Json.obj("id" -> authenticator.id , "authenticator" -> authenticator, "duration" -> Duration.Inf)
[error]                                                                                                            ^

[error] app\modules\MongoAuthenticatorRepository.scala:46: value id is not a member of type parameter JWTAuthenticator
[error] Error occurred in an application involving default arguments.
[error]     collection.flatMap(_.update(Json.obj("id" -> authenticator.id), obj, upsert = true))
[error]                                                                ^
[error] app\modules\MongoAuthenticatorRepository.scala:51: type mismatch;
[error]  found   : scala.concurrent.Future[reactivemongo.api.commands.WriteResult]
[error]  required: scala.concurrent.Future[Unit]
[error] Error occurred in an application involving default arguments.
[error]     collection.flatMap(_.remove(query))
[error]                                ^
[warn] app\modules\SilhouetteModule.scala:33: imported `MongoAuthenticatorRepository' is permanently hidden by definition of class MongoAuthenticatorRepository in package modules
[warn] import modules.{ MongoAuthenticatorRepository }
[warn]                  ^
[warn] one warning found
[error] 10 errors found
[error] (compile:compileIncremental) Compilation failed

#5

The JWTAuthenticator consists of properties which may need also JSON transformers. So you must make sure that you import these transformers before you declare your own transformer. Play comes with built-in transformers for Joda DateTime classes and Silhouette helps you with the FiniteDuration and the LoginInfo transformers.

The Play JSON API is a bit complicated for beginners, so reading the documentation is a good starting point.

Sometimes the ReactiveMongo API needs also specific JSON imports from the ReactiveMongo package. You could look in the MongoAuthInfoDAO.scala from the play-silhouette-persistence-reactivemongo package.

You can read more about the JSON API here.


#6

To your shadowing issue. Your MongoAuthenticatorRepository class is already located in the modules package so it’s automatically available for all classes in the same package, so you needn’t import the class in your SilhouetteModule.scala file, because it’s automatically imported. Therefore the compiler shows the warning.


#7

Thanks so much akkie, I think I am getting close. I have added the following to my MongoAuthenticatorRepository:

implicit object FiniteDurationFormat extends Format[FiniteDuration] {
    def reads(json: JsValue): JsResult[FiniteDuration] = LongReads.reads(json).map(_.seconds)
    def writes(o: FiniteDuration): JsValue = LongWrites.writes(o.toSeconds)
  }

  implicit val liformat = Json.format[LoginInfo]
  //implicit val jwyAuthenticatorformat = Json.format[JWTAuthenticator]

  implicit val jwtAuthenticatorWrites: Writes[JWTAuthenticator] = (
    (JsPath \ "id").write[String] and
    (JsPath \ "loginInfo").write[LoginInfo] and 
    (JsPath \ "lastUsedDateTime").write[DateTime] and 
    (JsPath \ "expirationDateTime").write[DateTime] and 
    (JsPath \ "idleTimeout").writeNullable[FiniteDuration] and 
    (JsPath \ "customClaims").writeNullable[JsObject]
  )(unlift(JWTAuthenticator.unapply))

  implicit val jwtAuthenticatorReads: Reads[JWTAuthenticator] = (
    (JsPath \ "id").read[String] and
    (JsPath \ "loginInfo").read[LoginInfo] and 
    (JsPath \ "lastUsedDateTime").read[DateTime] and 
    (JsPath \ "expirationDateTime").read[DateTime] and 
    (JsPath \ "idleTimeout").readNullable[FiniteDuration] and 
    (JsPath \ "customClaims").readNullable[JsObject]
  )(JWTAuthenticator.apply _)

  implicit val jwtAuthenticatorFormat: Format[JWTAuthenticator] = Format(jwtAuthenticatorReads, jwtAuthenticatorWrites)

Most of the errors seem to have been resolved. However this type mismatch error baffles me:

[error] two errors found
[error] (compile:compileIncremental) Compilation failed
[info] Compiling 2 Scala sources to \target\scala-2.11\classes...
[error] \app\models\daos\MongoAuthenticatorRepository.scala:52: type mismatch;
[error]  found   : com.mohiva.play.silhouette.impl.authenticators.JWTAuthenticator => (String, com.mohiva.play.silhouette.api.LoginInfo, org.joda.time.DateTime, org.joda.time.DateTime, Option[scala.concurrent.duration.FiniteDuration], Option[play.api.libs.json.JsObject])
[error]  required: JWTAuthenticator => (String, com.mohiva.play.silhouette.api.LoginInfo, org.joda.time.DateTime, org.joda.time.DateTime, Option[scala.concurrent.duration.FiniteDuration], Option[play.api.libs.json.JsObject])
[error]   )(unlift(JWTAuthenticator.unapply))
[error]           ^
[error] \app\models\daos\MongoAuthenticatorRepository.scala:61: type mismatch;
[error]  found   : (String, com.mohiva.play.silhouette.api.LoginInfo, org.joda.time.DateTime, org.joda.time.DateTime, Option[scala.concurrent.duration.FiniteDuration], Option[play.api.libs.json.JsObject]) => com.mohiva.play.silhouette.impl.authenticators.JWTAuthenticator
[error]  required: (String, com.mohiva.play.silhouette.api.LoginInfo, org.joda.time.DateTime, org.joda.time.DateTime, Option[scala.concurrent.duration.FiniteDuration], Option[play.api.libs.json.JsObject]) => JWTAuthenticator
[error]   )(JWTAuthenticator.apply _)
[error]                      ^
[error] two errors found
[error] (compile:compileIncremental) Compilation failed

Thank you once again for your assistance!