How to implement multiple Silhouette Authenticators?
Asked Answered
C

1

6

I used the play-silhouette-seed as a template for my application. So in my project, I use a cookie based authenticator (CookieAuthenticator). This works absolutely fine, even for REST calls via JavaScript which is embedded in my Twirl template. However, now I want to make also REST calls programmatically in clients other than a browser. As a consequence, I would have to retrieve the Set-Cookie: authenticator=... element on each response and set it as part of my request. In my JavaScript snippet which is embedded in my Twirl template and rendered in the browser, this is no problem because I don't have to deal with that, but for other clients (server etc.) this causes headaches.

I want to implement now a JWTAuthenticator in addition to my CookieAuthenticator. Is this even supported, or do I have to switch completely to JWTAuthenticator? Furthermore, do I need separate actions, even though everything should be the same implementation except the authenticator?

Cadmus answered 13/2, 2017 at 17:11 Comment(1)
I'm also exploring this possibility at the moment. Since one Env can only have one Authenticator, I believe you'll need 2 Env to host the 2 Authenticator. Therefore you'll need to separate your controller to use different Env.Deerstalker
J
3

Yes, Silhouette allows you to implement multiple authenticators. Here's how you can implement the JWTAuthenticator that provides its JWT authenticator service along with your CookieAuthenticator:

  1. As Douglas Liu, already pointed out in the comment, you will need to create an additional environment type. It should connect an Identity with the respective Authenticator.

For example:

trait CookieEnv extends Env {
  type I = Account
  type A = CookieAuthenticator
}

trait JWTEnv extends Env {
  type I = Account
  type A = JWTAuthenticator
}
  1. Implement the JWT bindings in your Silhouette module. Please take a look at play-silhouette-angular-seed for a full example.

For instance:

class SilhouetteModule extends AbstractModule with ScalaModule {

  def configure() {
    bind[Silhouette[CookieEnv]].to[SilhouetteProvider[CookieEnv]]
    bind[Silhouette[JWTEnv]].to[SilhouetteProvider[JWTEnv]]
    // ...
    ()
  }

   @Provides
  def provideCookieEnvironment(
                                userService: AccountService,
                                authenticatorService: AuthenticatorService[CookieAuthenticator],
                                eventBus: EventBus): Environment[CookieEnv] = {

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

  @Provides
  def provideJWTEnvironment(
                             userService: AccountService,
                             authenticatorService: AuthenticatorService[JWTAuthenticator],
                             eventBus: EventBus): Environment[JWTEnv] = {

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

// ...

  @Provides
  def provideCookieAuthenticatorService(
                                         @Named("authenticator-cookie-signer") cookieSigner: CookieSigner,
                                         @Named("authenticator-crypter") crypter: Crypter,
                                         fingerprintGenerator: FingerprintGenerator,
                                         idGenerator: IDGenerator,
                                         configuration: Configuration,
                                         clock: Clock): AuthenticatorService[CookieAuthenticator] = {

    val config = configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator")
    val encoder = new CrypterAuthenticatorEncoder(crypter)

    new CookieAuthenticatorService(config, None, cookieSigner, encoder, fingerprintGenerator, idGenerator, clock)
  }

  @Provides
  def provideJWTAuthenticatorService(
                                      @Named("authenticator-crypter") crypter: Crypter,
                                      idGenerator: IDGenerator,
                                      configuration: Configuration,
                                      clock: Clock): AuthenticatorService[JWTAuthenticator] = {

    val config = configuration.underlying.as[JWTAuthenticatorSettings]("silhouette.authenticator")
    val encoder = new CrypterAuthenticatorEncoder(crypter)

    new JWTAuthenticatorService(config, None, encoder, idGenerator, clock)
  }

// ...

}
  1. Add the JWTAuthenticator configuration settings to your silhouette.conf:

For example:

authenticator.fieldName = "X-Auth-Token"
authenticator.requestParts = ["headers"]
authenticator.issuerClaim = "Your fancy app"
authenticator.authenticatorExpiry = 12 hours
authenticator.sharedSecret = "!!!changeme!!!"
  1. Create a separate route for authentication via JWT:

For instance, in your app.routes file, add the following line:

# JWT Authentication
POST        /api/jwt/authenticate        controllers.auth.api.AuthController.authenticate
  1. Finally, in your AuthController, add the corresponding authenticate method.

Example code (adapted from SignInController.scala):

implicit val dataReads = (
  (__ \ 'email).read[String] and
    (__ \ 'password).read[String] and
    (__ \ 'rememberMe).read[Boolean]
  ) (SignInForm.SignInData.apply _)

def authenticate = Action.async(parse.json) { implicit request =>
  request.body.validate[SignInForm.SignInData].map { signInData =>
    credentialsProvider.authenticate(Credentials(signInData.email, signInData.password)).flatMap { loginInfo =>
      accountService.retrieve(loginInfo).flatMap {
        case Some(user) => silhouette.env.authenticatorService.create(loginInfo).map {
          case authenticator if signInData.rememberMe =>
            val c = configuration.underlying
            authenticator.copy(
              expirationDateTime = clock.now + c.as[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorExpiry"),
              idleTimeout = c.getAs[FiniteDuration]("silhouette.authenticator.rememberMe.authenticatorIdleTimeout")
            )
          case authenticator => authenticator
        }.flatMap { authenticator =>
          Logger.info(s"User ${user._id} successfully authenticated.")
          silhouette.env.eventBus.publish(LoginEvent(user, request))
          silhouette.env.authenticatorService.init(authenticator).map { token =>
            Ok(Json.obj("token" -> token))
          }
        }
        case None => Future.failed(new IdentityNotFoundException("Couldn't find user."))
      }
    }.recover {
      /* Login did not succeed, because user provided invalid credentials. */
      case e: ProviderException =>
        Logger.info(s"Host ${request.remoteAddress} tried to login with invalid credentials (email: ${signInData.email}).")
        Unauthorized(Json.obj("error" -> Messages("error.invalidCredentials")))
    }
  }.recoverTotal {
    case e: JsError =>
      Logger.info(s"Host ${request.remoteAddress} sent invalid auth payload. Error: $e.")
      Future.successful(Unauthorized(Json.obj("error" -> Messages("error.invalidPayload"))))
  }
}
Joliejoliet answered 11/3, 2017 at 8:43 Comment(5)
This is a great start but I fail to see why sillhouette's actions aren't used, to me it seems like it'd alleviate a great part of the work.Zicarelli
@Matthias - What is authenticator in authenticator.fieldName = "X-Auth-Token". Is it some global configuration object exposed by Silhouette? I have noticed the use of authenticator in documentation but I couldn't get my head around where it comes from? Is it a single object which is used for all types of authenticators?Camm
@ManuChadha It really doesn't matter how you name it. For instance, the first parameter of CookieAuthenticatorService expects an instance of type CookieAuthenticatorSettings, which is created in the method provideCookieAuthenticatorService located in the Silhouette module (see example provided as part of the answer).Joliejoliet
@Matthias - I didn't know about the typesafe config library. So silhouette.conf is a file in which I define various properties (eg authenticator.cookieName = "someName"). The file is compatible with typesafe config library and the file (and properties) could be read by a Configuration object (configuration.underlying.as[CookieAuthenticatorSettings]("silhouette.authenticator"). If I have two different authenticators (say credential and JWT) then to specify their configuration I'll need to create two separate groups (eg autheticatorCredential.cookieName and authenticatorJWT.fieldNameCamm
@ManuChadha That should work yes, provided that the parameters match the respective class definition. However, you could also keep authenticator as a base for both authenticators if you like.Joliejoliet

© 2022 - 2024 — McMap. All rights reserved.