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 request handlers and actions.
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]]
Request handlers
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.
Actions
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))
}
}
WebSockets
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)
}
}
}
Socket.io
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.
Error handlers
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.
Global error handlers
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 list of error handlers 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.
Local error handlers
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))
}
}
Exception handler
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.
List of error handlers
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.
SecuredErrorHandler
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 a401 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 a403 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)
}
}
UnsecuredErrorHandler
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)
}
}
Handle Ajax requests
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.
Non-standard 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)
}
}
Content negotiation
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