Stateful CookieAuthentication, problem with SecuredAction

Hi, I am try to use stateful cookie authenticator. I use play 2.5, silhouette 4.0, mysql, slick.

My configs:


Silhouette.conf
silhouette {
  authenticator {
    cookie {
      cookieName = "cookie_authenticator"
      cookiePath = "/"
      secureCookie = false
      httpOnlyCookie = true
      useFingerprinting = true
      cookieMaxAge = 30 minutes
      authenticatorIdleTimeout = 30 minutes
      authenticatorExpiry = 12 hours
    }
    signer.key = "[changeme]"
    crypter.key = "[changeme]"
  }
}
SilhouetteModule.scala
class SilhouetteModule extends AbstractModule with ScalaModule {

  def configure() {

    bind[UserService].to[UserServiceImpl]
    bind[Silhouette[MyEnvironment]].to[SilhouetteProvider[MyEnvironment]]

    bind[CacheLayer].to[PlayCacheLayer]
    bind[IDGenerator].toInstance(new SecureRandomIDGenerator())
    bind[PasswordHasher].toInstance(new BCryptPasswordHasher)
    bind[FingerprintGenerator].toInstance(new DefaultFingerprintGenerator(false))
    bind[EventBus].toInstance(EventBus())
    bind[Clock].toInstance(Clock())
  }

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

  //-------------- Environments -------------

  @Provides
  def provideRecoveryEnvironment(
    userService: UserService,
    authenticatorService: AuthenticatorService[CookieAuthenticator],
    eventBus: EventBus
  ): Environment[MyEnvironment] = {

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

  //-------------- Providers -------------

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

  //-------------- Authenticator Services -------------

  @Provides
  def provideCookieAuthenticatorService(
    @Named("authenticator-cookie-signer") cookieSigner: CookieSigner,
    @Named("authenticator-crypter") crypter: Crypter,
    fingerprintGenerator: FingerprintGenerator,
    idGenerator: IDGenerator,
    configuration: Configuration,
    authenticatorRepository: AuthenticatorRepository[CookieAuthenticator],
    clock: Clock): AuthenticatorService[CookieAuthenticator] = {
    val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator.cookie")
    val encoder = new CrypterAuthenticatorEncoder(crypter)
    new CookieAuthenticatorService(config, Some(authenticatorRepository), cookieSigner, encoder, fingerprintGenerator, idGenerator, clock)
  }

  //-------------

  @Provides
  @Named("authenticator-crypter")
  def provideAuthenticatorCrypter(configuration: Configuration): Crypter = {
    val config = configuration.underlying.as[JcaCrypterSettings]("silhouette.authenticator.crypter")
    new JcaCrypter(config)
  }

  @Provides
  def provideCookieSigner(configuration: Configuration): CookieSigner = {
    val config = configuration.underlying.as[JcaCookieSignerSettings]("silhouette.authenticator.jcasigner")
    new JcaCookieSigner(config)
  }


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

  @Provides @Named("authenticator-cookie-signer")
  def provideAuthenticatorCookieSigner(configuration: Configuration): CookieSigner = {
    val config = configuration.underlying.as[JcaCookieSignerSettings]("silhouette.authenticator.signer")
    new JcaCookieSigner(config)
  }

}
MyEnvironment.scala
trait MyEnvironment extends Env {
  type I = User
  type A = CookieAuthenticator
}

CookieAuthenticatorDao.scala
**// qCookieAuthenticators - TableQuery for store, retrieve, update, delete authenticator data**

class CookieAuthenticatorDao @Inject() (dbConfigProvider: DatabaseConfigProvider)
  extends AuthenticatorRepository[CookieAuthenticator] {
  lazy val dbConfig: DatabaseConfig[JdbcProfile] = dbConfigProvider.get[JdbcProfile]
  def find(id: String): Future[Option[CookieAuthenticator]] = {
    dbConfig.db.run(qCookieAuthenticators.withId(id).result.headOption).map({
      case Some(elem) => Some(elem)
      case _ => None
    })
  }
  def add(authenticator: CookieAuthenticator): Future[CookieAuthenticator] = {
    dbConfig.db.run(qCookieAuthenticators += authenticator).map(_ => authenticator)
  }
  def update(authenticator: CookieAuthenticator): Future[CookieAuthenticator] =
    dbConfig.db.run(qCookieAuthenticators.withId(authenticator.id).update(authenticator)).map(_ => authenticator)
  def remove(id: String): Future[Unit] =
    dbConfig.db.run(qCookieAuthenticators.withId(id).delete).map(_ => Unit)
}
UserService.scala
//    User extends Identity

trait UserService extends IdentityService[User] {
  def save(user: User): Future[User]
  def update(user: User): Future[User]
}

Implementation for UserService

class UserServiceImpl @Inject() (userDAO: UserDao) extends UserService {
  def retrieve(loginInfo: LoginInfo): Future[Option[User]] = userDAO.find(loginInfo)
  def save(user: User): Future[User] = userDAO.save(user)
  def update(user: User): Future[User] = userDAO.update(user)
}

So, when I access SecuredAction, silhouette retrieve CookieAuthenticator from data base it’s ok, but after that dont retrieve User by loginInfo he not invoke UserService. Without SecuredAction, using UserAwareAction I can get CookieAuthenticator ID but I think is not correct

ApplicationController.scala
class ApplicationController @Inject() (
  val messagesApi: MessagesApi,
  silhouette: Silhouette[MyEnvironment]
)
  extends Controller with I18nSupport {
  def user: Action[AnyContent] = silhouette.SecuredAction.async { implicit request =>
    Future.successful(Ok(Json.toJson(request.identity)))
  }
}

As I understood silhouette inject auth data in request using SecuredRequestHandlerBuilder

Invoker
  override def invokeBlock[B, T](block: SecuredRequest[E, B] => Future[HandlerResult[T]])(implicit request: Request[B]): Future[HandlerResult[T]] = {
    withAuthorization(handleAuthentication).flatMap {
      // A user is both authenticated and authorized. The request will be granted
      case (Some(authenticator), Some(identity), Some(authorized)) if authorized =>
        environment.eventBus.publish(AuthenticatedEvent(identity, request))
        handleBlock(authenticator, a => block(SecuredRequest(identity, a, request)))
      // A user is authenticated but not authorized. The request will be forbidden
      case (Some(authenticator), Some(identity), _) =>
        environment.eventBus.publish(NotAuthorizedEvent(identity, request))
        handleBlock(authenticator, _ => errorHandler.onNotAuthorized.map(r => HandlerResult(r)))
      // An authenticator but no user was found. The request will ask for authentication and the authenticator will be discarded
      case (Some(authenticator), None, _) =>
        environment.eventBus.publish(NotAuthenticatedEvent(request))
        for {
          result <- errorHandler.onNotAuthenticated
          discardedResult <- environment.authenticatorService.discard(authenticator.extract, result)
        } yield HandlerResult(discardedResult)
      // No authenticator and no user was found. The request will ask for authentication
      case _ =>
        environment.eventBus.publish(NotAuthenticatedEvent(request))
        errorHandler.onNotAuthenticated.map(r => HandlerResult(r))
    }
  }

// handleAuthentication block

protected def handleAuthentication[B](implicit request: Request[B]): Future[(Option[Either[E#A, E#A]], Option[E#I])] = {
   environment.authenticatorService.retrieve.flatMap {
      // A valid authenticator was found so we retrieve also the identity
      //                      get here, but not invoke environment.identityService.retrieve(a.loginInfo).map(i => Some(Left(a)) -> i)
      //                      a.isValid get True by configs from silhouette.conf
      case Some(a) if a.isValid  => environment.identityService.retrieve(a.loginInfo).map(i => Some(Left(a)) -> i)
      // An invalid authenticator was found so we needn't retrieve the identity
      case Some(a) if !a.isValid => Future.successful(Some(Left(a)) -> None)
      // No authenticator was found so we try to authenticate with a request provider
      case None => handleRequestProviderAuthentication.flatMap {
        // Authentication was successful, so we retrieve the identity and create a new authenticator for it
        case Some(loginInfo) => environment.identityService.retrieve(loginInfo).flatMap { i =>
          environment.authenticatorService.create(loginInfo).map(a => Some(Right(a)) -> i)
        }
        // No identity and no authenticator was found
        case None => Future.successful(None -> None)
      }
    }
  }

I think problem in Environment[MyEnvironment] initialization when we use stateful CookieAuthenticator.

I tried to change from CookieAtuhenticator to JWTAuthenticator and it’s ok works well with same configs with little changes in dao and added service for JWT, but I need CookieAuthenticator for web client.

Please help.

###Thank you


Hi,

have you tried to turn on logging? The cookie authenticator DAO returns the authenticator from database? The returned authenticator contains a LoginInfo for which a user is stored? The authenticator is valid and not expired? The validity of the authenticator will be checked after the token was retrieved from the database. The fingerprints are the same? Maybe you disable fingerprinting for testing.

Best regards,
Christian

"The fingerprints are the same?"
this is my problem, thank you, excuse me for disturbing. Silhouette ROCK :smiley: