Two Error Handlers for JSON and HTML at the same time


#1

Hello There! I’ve been working with silhouette for the past two days and it’s beautiful.

Just wanted to know if there’s a way to use the library according to my scenario.

I have two different namespaces in the app, one for a rich REST API with restful endpoints returning json documents and leveraging HTTP status codes to control how other clients integrate with our data. For these I have created a JWTAuthenticationEnv class which implements stateless auth with JWTs which is working just fine.

The other is a set of regular Play MVC controllers where we make full use of stateful auth.

I want to define two different SecuredErrorHandler behaviors: one for the REST API and one for the MVC stuff. I want to redirect all unauthenticated and unauthorized users to the login page on web, and on the REST API I want to define a series of JSON responses with their RFC-standard status codes.

So far I have managed to implement both of these behaviors really easily:

class CustomSecuredErrorHandler extends SecuredErrorHandler {

  override def onNotAuthenticated(implicit request: RequestHeader) = {
    Future.successful(Redirect(web.access.controllers.routes.AccessController.signIn()))
  }

  override def onNotAuthorized(implicit request: RequestHeader) = {
    Future.successful(Redirect(web.access.controllers.routes.AccessController.signIn()).flashing("error" -> Messages("access.denied")))
  }
}
class JWTAuthenticationErrorHandler extends SecuredErrorHandler {
    override def onNotAuthenticated(implicit request: RequestHeader) = {
        Future.successful(Unauthorized(Json.toJson(Map[String, String]("status" -> "We don't recognize who you are. Please authenticate."))))
    }

    override def onNotAuthorized(implicit request: RequestHeader) = {
        Future.successful(Forbidden(Json.toJson(Map[String, String]("status" -> "You're authenticated but you're not authorized for this resource."))))
    }
}

But what I want to know is, can I bind them both at the SilhouetteModule level? I can’t just add another line on SilhouetteModule.scala like this:

//doesn't work
def configure() {
...
    bind[SecuredErrorHandler].to[CustomSecuredErrorHandler]
    bind[SecuredErrorHandler].to[JWTAuthenticationErrorHandler]
...

Or rather how can I make a single Error handler which knows how to react depending on what type of media the client Accepts? I think that the best approach would be to create a single class where each of the overriden methods evaluate against the Accept header of every request I receive and decide on what document (HTML or JSON) to send back depending on it, however this probably isn’t going to work because I don’t always get an Accept header.

If there’s no viable solution to what I’m asking, is there a better way to do this?

Thank you!


#2

Btw I figured it out. You need to override the onNotAuthenticated, onNotAuthorized, produceResponse and toJsonError methods at the ErrorHandler level for your CustomSecuredErrorHandler. You can actually see the request that’s producing the exception at that level, due to non-Authentication or non-Authorization by the client. Then it’s just a simple matter of pattern matching against what content the client Accepts. You can set up a variety of traits to do that:

class CustomSecuredErrorHandler @Inject() (val messagesApi: MessagesApi) extends SecuredErrorHandler 
    with CustomNotAuthenticatedErrorHandler
    with CustomNotAuthorizedErrorHandler{

  override def exceptionHandler(implicit request: RequestHeader): PartialFunction[Throwable, Future[Result]] = {
    super[CustomNotAuthenticatedErrorHandler].exceptionHandler
  }
}

trait CustomNotAuthenticatedErrorHandler
    extends NotAuthenticatedErrorHandler
    with CustomErrorHandler{

  override def exceptionHandler(implicit request: RequestHeader) = {
    case e: NotAuthenticatedException =>
      super.exceptionHandler(request)(e)
  }

  override def onNotAuthenticated(implicit request: RequestHeader) = {
     produceResponse(Unauthorized, Messages("not.authenticated"))
  }
}

trait CustomNotAuthorizedErrorHandler
    extends NotAuthorizedErrorHandler
    with CustomErrorHandler{

  override def exceptionHandler(implicit request: RequestHeader) = {
    case e: NotAuthorizedException =>
      super.exceptionHandler(request)(e)
  }

  override def onNotAuthorized(implicit request: RequestHeader) = {
    produceResponse(Forbidden, Messages("not.authorized"))
  }
}

trait CustomErrorHandler
    extends DefaultErrorHandler {

  protected override def produceResponse[S <: Status](status: S, msg: String)(implicit request: RequestHeader): Future[Result] = {
    import Codec._
    Future.successful(render {
      case Accepts.Json() => status(toJsonError(msg))
      case Accepts.Html() => Redirect( web.access.controllers.routes.AccessController.signIn() )
      case _ => status(toPlainTextError(msg))
    })
  }

  override def toJsonError(message: String) = Json.obj("status" -> false, "message" -> message)
  override def toPlainTextError(message: String) = message
}

#3

Correct! This is also documented and the default error handler uses the same approach.