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 implement the Silhouette
controller.
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator]
This controller provides the Silhouette Environment and it defines the Identity and the Authenticator the endpoints can handle.
Request Handlers
New in version 2.0
The base implementations to handle secured endpoints are encapsulated into request handlers which 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 your 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(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* An example for a secured request handler.
*/
def securedRequestHandler = Action.async { implicit request =>
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 user aware request handler.
*/
def userAwareRequestHandler = Action.async { implicit request =>
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 a global or local fallback handler.
Actions
Silhouette provides a replacement for Play’s built in Action class named SecuredAction
which is based on the SecuredRequestHandler
.
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Renders the index page.
*
* @returns The result to send to the client.
*/
def index = SecuredAction { implicit request =>
Ok(views.html.index(request.identity))
}
}
There is also a UserAwareAction
which is based on the UserAwareRequestHandler
.
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Renders the index page.
*
* @returns The result to send to the client.
*/
def index = UserAwareAction { implicit request =>
val userName = request.identity match {
case Some(identity) => identity.fullName
case None => "Guest"
}
Ok("Hello %s".format(userName))
}
}
WebSockets
New in version 2.0
With Silhouette it'a also possible to secure WebSockets with the help of the SecuredRequestHandler
or the UserAwareRequestHandler
. Please take a look on the following examples to see how this can be implemented.
WebSockets with actors
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(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
def socket = WebSocket.tryAcceptWithActor[String, String] { request =>
implicit val req = Request(request, AnyContentAsEmpty)
SecuredRequestHandler { securedRequest =>
Future.successful(HandlerResult(Ok, Some(securedRequest.identity)))
}.map {
case HandlerResult(r, Some(user)) => Right(MyWebSocketActor.props(user) _)
case HandlerResult(r, None) => Left(r)
}
}
}
WebSockets with iteratees
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
def socket = WebSocket.tryAccept[JsValue] { request =>
implicit val req = Request(request, AnyContentAsEmpty)
SecuredRequestHandler { securedRequest =>
Future.successful(HandlerResult(Ok, Some(securedRequest.identity)))
}.map {
case HandlerResult(_, Some(_)) => Right((ws.in, ws.out))
case HandlerResult(r, None) => Left(r)
}
}
}
Fallback handler
If the access to a secured endpoint will be denied then it's possible to provide a fallback handler to handle the incoming request and return an appropriate result.
Global Fallback
You can mix the SecuredSettings
trait into your Global
object. This trait provides a method called onNotAuthenticated
. If you implement this method, then every time a user calls a restricted endpoint, the result specified in the global fallback method will be returned.
object Global extends GlobalSettings with SecuredSettings {
/**
* 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.
* @param lang The currently selected language.
* @return The result to send to the client.
*/
override def onNotAuthenticated(request: RequestHeader, lang: Lang) = {
Some(Future.successful(Unauthorized("No access")))
}
}
Local Fallback
Every controller which is derived from the Silhouette
base controller has a method called onNotAuthenticated
. If you override these method, then you can return a not-authenticated result similar to the global fallback but only for this specific controller. The local fallback has
precedence over the global fallback.
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Implement this to return a result when the 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(request: RequestHeader): Option[Future[Result]] = {
Some(Future.successful(Unauthorized("No access")))
}
/**
* Renders the index page.
*
* @returns The result to send to the client.
*/
def index = SecuredAction { implicit request =>
Ok(views.html.index(request.identity))
}
}
Note
If you don’t implement one or both of the fallback methods, a 401 response with a simple message will be displayed to the user.
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 and inside a fallback method for unauthenticated users.
The JavaScript part with JQuery
$.ajax({
headers: { 'IsAjax': 'true' },
...
});
The Play part with a local fallback method for unauthenticated users
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Implement this to return a result when the 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(request: RequestHeader): Option[Future[Result]] = {
val result = request.headers.get("IsAjax") match {
case Some("true") => Json.obj("result" -> "No access")
case _ => "No access"
}
Some(Future.successful(Unauthorized(result)))
}
/**
* 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 local or global fallback methods 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 and inside a fallback method for unauthenticated users.
The JavaScript part with JQuery
$.ajax({
headers: {
'Accept': 'application/json; charset=utf-8',
'Content-Type': 'application/json; charset=utf-8'
},
...
})
The Play part with a local fallback method for unauthenticated users
class Application(env: Environment[User, CookieAuthenticator])
extends Silhouette[User, CookieAuthenticator] {
/**
* Implement this to return a result when the 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(request: RequestHeader): Option[Future[Result]] = {
val result = render {
case Accepts.Json() => Json.obj("result" -> "No access")
case Accepts.Html() => "No access"
}
Some(Future.successful(Unauthorized(result)))
}
/**
* 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