Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement multiple Silhouette Authenticators?

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?

like image 468
John Doe Avatar asked Feb 13 '17 17:02

John Doe


1 Answers

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"))))
  }
}
like image 181
Matthias A. Eckhart Avatar answered Nov 17 '22 12:11

Matthias A. Eckhart