Endpoints

This page describes the available mechanisms to secure your endpoints.

An endpoint in Play is either an Action or a WebSocket and Silhouette provides mechanisms to secure both of them. What all mechanisms share is the necessity to inject the Silhouette stack into your controller.

πŸ“˜

Info

The DefaultEnv type we use in the following examples is only an example type. Please refer to the environment type section to see how an environment type can be created.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller

This controller provides the Silhouette stack with the Environment for the defined environment type and all the available and .

To bind the Silhouette stack for an Environment you can use the SilhouetteProvider. This class is a base implementation of the Silhouette trait wich can be instantiated by passing all the needed dependencies.

val silhouette = new SilhouetteProvider[DefaultEnv](...)

The following example shows how you can create an instance of the SilhouetteProvider with the help of the Guice dependency injection framework. Please consult the documentation of your favorite dependency injection framework, to see how you can bind a class to a trait.

bind[Silhouette[DefaultEnv]].to[SilhouetteProvider[DefaultEnv]]

In Silhouette, request handlers are the foundation to handle secured endpoints and the building blocks for the more specific action types. A request handler can execute an arbitrary block of code and must return a HandlerResult . This HandlerResult consists of a normal Play result and arbitrary additional data which can be transported out of these handlers.

There exists a SecuredRequestHandler which intercepts requests and checks if there is an authenticated user. If there is one, the execution continues and the enclosed code is invoked.

The UnsecuredRequestHandler does the opposite of the SecuredRequestHandler. It intercepts requests and checks if there is a not-authenticated user. If there is one, the execution continues and the enclosed code is invoked.

There is also a UserAwareRequestHandler that can be used for endpoints that need to know if there is a current user but can be executed even if there isn't one.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {
    
  /**
   * An example for a secured request handler.
   */
  def securedRequestHandler = Action.async { implicit request =>
    silhouette.SecuredRequestHandler { securedRequest =>
      Future.successful(HandlerResult(Ok, Some(securedRequest.identity)))
    }.map {
      case HandlerResult(r, Some(user)) => Ok(Json.toJson(user.loginInfo))
      case HandlerResult(r, None) => Unauthorized
    }
  }

  /**
   * An example for an unsecured request handler.
   */
  def unsecuredRequestHandler = Action.async { implicit request =>
    silhouette.UnsecuredRequestHandler { _ =>
      Future.successful(HandlerResult(Ok, Some("some data")))
    }.map {
      case HandlerResult(r, Some(data)) => Ok(data)
      case HandlerResult(r, None) => Forbidden
    }
  }
  
  /**
   * An example for a user-aware request handler.
   */
  def userAwareRequestHandler = Action.async { implicit request =>
    silhouette.UserAwareRequestHandler { userAwareRequest =>
      Future.successful(HandlerResult(Ok, userAwareRequest.identity))
    }.map {
      case HandlerResult(r, Some(user)) => Ok(Json.toJson(user.loginInfo))
      case HandlerResult(r, None) => Unauthorized
    }
  }
}

πŸ“˜

Note

For unauthenticated users you can implement global or local error handlers.

Silhouette provides a replacement for Play’s built in Action class named SecuredAction which is based on the SecuredRequestHandler.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * Renders the index page.
   *
   * @returns The result to send to the client.
   */
  def index = silhouette.SecuredAction { implicit request =>
    Ok(views.html.index(request.identity))
  }
}

The opposite of the SecuredAction is the UnsecuredAction which is based on the UnsecuredRequestHandler.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * Renders the sign-in page.
   *
   * @returns The result to send to the client.
   */
  def signIn = silhouette.UnsecuredAction { implicit request =>
    Ok(views.html.signIn)
  }
}

There is also a UserAwareAction which is based on the UserAwareRequestHandler.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * Renders the index page.
   *
   * @returns The result to send to the client.
   */
  def index = silhouette.UserAwareAction { implicit request =>
    val userName = request.identity match {
      case Some(identity) => identity.fullName
      case None => "Guest"
    }
    Ok("Hello %s".format(userName))
  }
}

With Silhouette it'a also possible to secure WebSockets with the help of the SecuredRequestHandler or the UserAwareRequestHandler. Please take a look at the following examples to see how this can be implemented.

import akka.actor.{ Actor, ActorRef, ActorSystem, Props }
import akka.stream.Materializer
import com.mohiva.play.silhouette.api.{ HandlerResult, Silhouette }
import models.User
import play.api.libs.streams.ActorFlow
import play.api.mvc.{ AnyContentAsEmpty, Controller, Request, WebSocket }
import utils.auth.DefaultEnv

import scala.concurrent.{ ExecutionContext, Future }

object MyWebSocketActor {
  def props(user: User)(out: ActorRef) = Props(new MyWebSocketActor(user, out))
}

class MyWebSocketActor(user: User, out: ActorRef) extends Actor {
  def receive = {
    case msg: String =>
      out ! (s"Hi ${user.name}, I received your message: " + msg)
  }
}

class Application(silhouette: Silhouette[DefaultEnv])(
  implicit
  system: ActorSystem,
  materializer: Materializer,
  ec: ExecutionContext
) extends Controller {

  def socket = WebSocket.acceptOrResult[String, String] { request =>
    implicit val req = Request(request, AnyContentAsEmpty)
    silhouette.SecuredRequestHandler { securedRequest =>
      Future.successful(HandlerResult(Ok, Some(securedRequest.identity)))
    }.map {
      case HandlerResult(r, Some(user)) => Right(ActorFlow.actorRef(MyWebSocketActor.props(user)))
      case HandlerResult(r, None) => Left(r)
    }
  }
}
class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  def socket = WebSocket.tryAccept[JsValue] { request =>
    implicit val req = Request(request, AnyContentAsEmpty)
    silhouette.SecuredRequestHandler { securedRequest =>  
      Future.successful(HandlerResult(Ok, Some(securedRequest.identity)))
    }.map {
      case HandlerResult(_, Some(_)) => Right((ws.in, ws.out))
      case HandlerResult(r, None)    => Left(r)
    }
  }
}

Integrating with socket.io with Silhouette is very similar to native Play! WebSockets as above. For socket.io support, you need to bring in this repo to your project.

Using Guice, all you need to do is to bring in the Silhouette environment into your engine provider and use a similar technique as above in the onConnectAsync block. In your configure() block in your Guice module, add a singleton provider:

override def configure(): Unit = {
    bind[EngineIOController].toProvider[SocketIOEngineProvider]
  }

And then in your SocketIOEngineProvider, you can do something like the following:

@Singleton
class SocketIOEngineProvider @Inject()(
  socketIO: SocketIO,
  silhouette: Silhouette[DefaultEnv]
)(
  implicit 
  mat: Materializer
) extends Provider[EngineIOController] with Logger {

  override lazy val get: EngineIOController = {
    socketIO.builder
      .onConnectAsync { (request, _) =>
        {
          implicit val req = Request(request, AnyContentAsEmpty)
          silhouette
            .SecuredRequestHandler { securedRequest =>
              Future.successful(
                HandlerResult(Ok, Some(securedRequest.identity)))
            }
            .map {
              case HandlerResult(_, Some(user)) => user
              case HandlerResult(_, None) =>
                throw new NotAuthenticatedException("User is not authenticated")
            }
        }
      }
      .withErrorHandler {
        case _: NotAuthenticatedException => JsString("You are not authenticated!")
      }
      .createController()
  }
}

You can use the user returned by onConnectAsync later in perhaps defaultNamespace or a custom namespace. See the play-socket.io docs for details on this.

If the access to an endpoint will be denied then it's possible to provide an error handler to handle the incoming request and return an appropriate result. Every request handler may provide it's own error handler implementation. No worry - we'll provide a detailed overview in the next section.

The global error handlers are the default bound error handlers which will be provided by your DI framework if the request handler instances will be wired. By default, Silhouette provides default DI modules for the request handlers with their appropriate error handler implementations.

To disable the default modules you must append the modules to the play.modules.disabled property in your application.conf. Following comes an example that shows how a module can be disabled. Please refer to the section for an overview of all available error handlers and their modules.

play.modules.disabled += "com.mohiva.play.silhouette.api.actions.SecuredErrorHandlerModule"

If you have the default error handler module disabled, then you can bind your own instance by creating a new module or by adding the binding to your default DI module. This process is detailed documented in the Play Framework documentation.

A local error handler can be directly passed to your endpoint. It overrides the default injected error handler with a custom one and you are able to define different error handlers inside a single controller.

class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * A local error handler.
   */
  val errorHandler = new SecuredErrorHandler {
    override def onNotAuthenticated(implicit request: RequestHeader) = {
      Future.successful(Unauthorized("local.not.authenticated"))
    }
    override def onNotAuthorized(implicit request: RequestHeader) = {
      Future.successful(Forbidden("local.not.authorized"))
    }
  }

  /**
   * Renders the index page.
   *
   * @returns The result to send to the client.
   */
  def index = silhouette.SecuredAction(errorHandler) { implicit request =>
    Ok(views.html.index(request.identity))
  }
}

Every error handler has a method called exceptionHandler which calls the error handler methods based on a caught exception.

def exceptionHandler(implicit request: RequestHeader): PartialFunction[Throwable, Future[Result]]

This method can be overridden to delegate user defined exceptions to the appropriate status codes.

Not every request handler has it's own error handler type. The UserAwareRequestHandler as example doesn't need an error handler because the access to this request handler is always granted. So following we list all error handlers with their appropriate request handler types.

The SecuredErrorHandler handles the errors for the SecuredRequestHandler and its derived SecuredAction type. It can handle two types of errors:

  • The first error is the not authenticated error which appears if a not authenticated user tries to access a secured endpoint. In this case the error handler should return a 401 Unauthorized status code as defined in the RFC 7235.

  • The second error is the not authorized error which appears if a user is authenticated but not authorized. In this case the error handler should return a 403 Forbidden status code as defined in the RFC 7231.

To define your own implementation, create a class derived from the SecuredErrorHandler trait and register it with your DI framework.

class CustomSecuredErrorHandler extends SecuredErrorHandler {
  
  /**
   * Called when a user is not authenticated.
   *
   * As defined by RFC 2616, the status code of the response should be 401 Unauthorized.
   *
   * @param request The request header.
   * @return The result to send to the client.
   */
  override def onNotAuthenticated(implicit request: RequestHeader) = {
    Future.successful(Unauthorized)
  }
  
  /**
   * Called when a user is authenticated but not authorized.
   *
   * As defined by RFC 2616, the status code of the response should be 403 Forbidden.
   *
   * @param request The request header.
   * @return The result to send to the client.
   */
  override def onNotAuthorized(implicit request: RequestHeader) = {
    Future.successful(Forbidden)
  }
}

The UnsecuredErrorHandler handles the errors for the UnsecuredRequestHandler and its derived UnsecuredAction type. It can handle a not authorized error which appears if a user is authenticated but not authorized. In this case the error handler should return a 403 Forbidden status code as defined in the RFC 7231.

To define your own implementation, create a class derived from the UnsecuredErrorHandler trait and register it with your DI framework.

class CustomUnsecuredErrorHandler extends UnsecuredErrorHandler {
  
  /**
   * Called when a user is authenticated but not authorized.
   *
   * As defined by RFC 2616, the status code of the response should be 403 Forbidden.
   *
   * @param request The request header.
   * @return The result to send to the client.
   */
  override def onNotAuthorized(implicit request: RequestHeader) = {
    Future.successful(Forbidden)
  }
}

Applications that accept both Ajax and normal requests should likely provide a JSON result to the first and a different result to others. There are two different approaches to achieve this. The first approach uses a non-standard HTTP request header. The Play application can check for this header and respond with a suitable result. The second approach uses Content negotiation to serve different versions of a document based on the ACCEPT request header.

The example below uses a non-standard HTTP request header inside a secured action.

$.ajax({
    headers: { 'IsAjax': 'true' },
    ...
});
class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * Renders the index page.
   *
   * @returns The result to send to the client.
   */
  def index = SecuredAction { implicit request =>
    val result = request.headers.get("IsAjax") match {
      case Some("true") => Json.obj("identity" -> request.identity)
      case _ => views.html.index(request.identity)
    }

    Ok(result)
  }
}

By default Silhouette supports content negotiation for the most common media types: text/plain, text/html, application/json and application/xml. So if no custom error handlers are implemented, Silhouette responds with the appropriate response based on the ACCEPT header defined by the user agent. The response format will default to plain text in case the request does not match one of the known media types. The example below uses content negotiation inside a secured action.

$.ajax({
    headers: {
        'Accept': 'application/json; charset=utf-8',
        'Content-Type': 'application/json; charset=utf-8'
    },
    ...
})
class Application(silhouette: Silhouette[DefaultEnv]) extends Controller {

  /**
   * Renders the index page.
   *
   * @returns The result to send to the client.
   */
  def index = SecuredAction { implicit request =>
    val result = render {
      case Accepts.Json() => Json.obj("identity" -> request.identity)
      case Accepts.Html() => views.html.index(request.identity)
    }
    Ok(result)
  }
}

Updated less than a minute ago


What's Next

Filters