Action Composition in Play Framework
Action composition in Play Framework is an incredibly powerful way to enhance or restrict controller behaviour, for example to implement authentication or authorisation controls, set default headers, or handle OPTIONS requests.
But typical action composition can be messy. Using action builders, we can simplify the process - and you may have already used them without realising it!
You’ve probably seen code like this before, it’s pretty standard stuff:
def index = Action { request =>
Ok(views.html.index("Your new application is ready."))
}
And if you’ve used Play Framework asynchronously, maybe something like this:
def index = Action.async { request =>
doSomething map { result =>
Ok(views.html.index("Your new application is ready."))
}
}
You can also easily parse the request using a different content type (or “body parser”), for example using JSON:
def index = Action.async(parse.json) { request =>
doSomething map { result =>
Ok(Json.obj(result -> "Your new application is ready."))
}
}
All of these use the Action action builder (that is, the Action object, which is an action builder).
By creating a new action builder, we can create a drop-in replacement for the Action calls (both Action
and Action.async
), while still supporting the body parser parameter.
Creating a new action builder
Since Action
is just an implementation of ActionBuilder[Request]
, we can extend ActionBuilder
to use in place of Action
.
ActionBuilder requires that we implement invokeBlock
, and that’s where the magic happens. This is a bare minimum implementation, and its exactly what Action
does for us already.
invokeBlock
takes two parameters, the first is the incoming request, and the second is the function body, taking Request[A]
as a parameter and returning a Future[SimpleResult]
object Interceptor extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = block(request)
}
It doesn’t do much (in fact, nothing different from Action
), but now we can use that in our controller instead:
def index = Interceptor.async(parse.json) { request =>
doSomething map { result =>
Ok(Json.obj(result -> "Your new application is ready."))
}
}
And it works using the same syntax:
def index = Interceptor { request => Ok }
def index = Interceptor.async { request => future { Ok } }
def index = Interceptor(parse.json) { request => Ok }
def index = Interceptor.async(parse.json) { request => future { Ok } }
Intercepting requests
There’s many reasons to intercept a request before it reaches your controller - authentication, authorisation, rate limiting or performance monitoring - for this example, we’ll use authentication.
There’s also many ways to authenticate a request - using headers, cookies, etc. - and while this is one way you certainly wouldn’t do it, it works for a demo:
object Authenticated extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = {
if(request.headers.get("Authorization").isDefined)
block(request)
else
future { Results.Status(Status.UNAUTHORIZED) }
}
}
This very simple example checks for an Authorization header.
If it’s there, it calls block(request)
and request processing continues as expected (don’t confuse the word “block” to mean the request gets blocked, we’re actually executing the code block or function body we were passed earlier).
If the Authorization header isn’t found, it returns an Unauthorized (401) response, using Results.Status()
.
At this point we could have returned any Future[SimpleResult]
we like. We could look up data in memcached, MongoDB or call a remote API using OAuth2 - and either let the request continue, or return an appropriate response instead.
But this isn’t ideal - we’ve got our action builder sending a response to the client. We need to pass that responsibility back to the controller.
Passing user context
So far, we’ve intercepted the request using action composition, but once we get to the controller code we don’t know who the user is, and no way to find out if we couldn’t establish the users identity.
There’s plenty of ways to fix this - some of them documented in the Play Framework action composition documentation - but we’ll go with wrapping the request class.
This has the advantage of keeping all existing code ‘compatible’ - we can simply search and replace Action for Authenticated and every endpoint is protected.
For a comparison, here’s what the controller would have looked like if we’d wrapped the action:
def index = Authenticated { user =>
Action { request =>
Ok(user.get)
}
}
And here’s what we’re going to create with our custom request class:
def index = Authenticated { request =>
Ok(request.user.get)
}
Wrapping the request class
To wrap the request class, and be able to access the user object from our controllers without casting the request first, we need to create a new trait and a new object.
The trait AuthenticatedRequest
simply extends Request
and adds a user value:
trait AuthenticatedRequest[+A] extends Request[A] {
val user: Option[JsObject]
}
The object AuthenticatedRequest
is similar to play.api.mvc.Http.Request
- except we copy the existing request, and add the user value:
object AuthenticatedRequest {
def apply[A](u: Option[JsObject], r: Request[A]) = new AuthenticatedRequest[A] {
def id = r.id
def tags = r.tags
def uri = r.uri
def path = r.path
def method = r.method
def version = r.version
def queryString = r.queryString
def headers = r.headers
lazy val remoteAddress = r.remoteAddress
def username = None
val body = r.body
val user = u
}
}
Next, we need to change our call to block(request)
to pass through our new AuthenticatedRequest
object.
To make this work, we also need to change some of the Request
types to AuthenticatedRequest
in our Authenticated
object. We’ve also let the request continue even without a valid user - we can use this from a controller to know the users identity couldn’t be established.
Here it is in full:
object Authenticated extends ActionBuilder[AuthenticatedRequest] {
def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[SimpleResult]) = {
if(request.headers.get("Authorization").isDefined)
block(AuthenticatedRequest[A](Some(Json.obj()), request))
else
block(AuthenticatedRequest[A](None, request))
}
}
Notice that invokeBlock still expects a Request[A]
as its request parameter, but now the block parameter defines a function with a AuthenticatedRequest[A]
parameter instead.
For now, our user is nothing more than an empty JsObject. In a real application, it could be any type (not just JsObject), and the value for user could come from anywhere (e.g. a database, session, OAuth2, etc).
When authentication fails, we pass through None
, letting the controller know that no user could be found.
From our controller, we can now access the user object with request.user
:
def index = Authenticated.async { request =>
future { Ok(request.user.get) }
}
Authorisation
So far we’ve seen how to require authentication using an action builder. While this is useful if your app uses an ‘all or nothing’ security model, this isn’t particularly useful for authorisation, for example in role based security.
There’s two easy ways to solve this problem:
Create another action builder to wrap our Authenticated builder
We can wrap our Authenticated action builder with another builder, giving us controller code that might look something like this:
def index = Authorised(roles = List("blog.post")) { request =>
Ok(request.user.get)
}
This is very straightforward, but creates an unnecessary dependency between your authorisation and authentication code.
Use normal action composition to require authorisation
Using action composition, we might end up with code that looks like this:
def index = Authenticated { request =>
Authorised(roles = List("blog.post")) {
Ok(request.user.get)
}
}
And we can also do this:
def index = Authorised(roles = List("blog.post")) { request =>
Ok(request.user.get)
}
The downside to normal action composition is obvious once you start using different body parsers or asynchronous operations:
def index = Authenticated(parse.json).async { request =>
Authorised(parse.json).async(roles = List("blog.post")) {
future { Ok(request.user.get) }
}
}
On every nested action we are required to redeclare the body parser and call async.
But this is Scala - there’s a nicer way!
Neither of those examples are particularly suitable. Neither will cleanly handle a negative outcome (either needing some messy code or hiding the unauthorised response away in a helper class), and neither of them nest well.
Whenever you need to check authorisation, the outcome is normally one of two things - in the case of a web application, its likely that both of them will end in returning some content to the user.
In true MVC style, this shouldn’t be the responsibility of the authorisation code. It should be in the controller.
We could just use an if/then/else statement, but I like something a bit cleaner:
def index = Authenticated { request =>
Authorised(request, request.user) {
Ok(request.user.get)
} otherwise {
Unauthorized
}
}
And our Authorised
implementation is simple. We provide Authorised
and Authorised.async
functions, and return an instance of our Authorised
class providing an otherwise
method.
object Authorised {
def async[T](request: Request[T], user: Option[JsObject]) = {
(block: Future[SimpleResult]) => new Authorised[T](request, user, block)
}
def apply[T](request: Request[T], user: Option[JsObject]) = {
(block: SimpleResult) => new Authorised[T](request, user, future { block })
}
}
class Authorised[T](request: Request[T], user: Option[JsObject], success: Future[SimpleResult]) {
def authorised = {
if(user.isDefined) true else false
}
def otherwise(block: Future[SimpleResult]) : Future[SimpleResult] = authorised.flatMap { valid => if (valid) success else block }
def otherwise(block: SimpleResult) : SimpleResult = if(authorised.value.get.get) success.value.get.get else block
}
As with the Authentication action builder, this implementation isn’t particularly secure. As long as the user is defined (which it will be if the Authorization header is set), then authorisation is successful.
In this example we’ve also passed the request object to the authorisation layer. It would be cleaner to abstract the request from our authorisation code using named roles or permissions.
Action builder vs helper object
There’s a reason we’ve created authentication as an action builder but kept authorisation as a helper object.
Authentication is a one-time process (for each request) - it rarely needs to be done multiple times, and it rarely changes once the request has been received. Even if we can’t establish the identity of the user, that doesn’t necessarily mean authentication has failed or that the user has no permissions.
Authorisation can happen multiple times within one request, in one controller, and with distinct outcomes - for example, rendering a dashboard might perform multiple unique authorisation checks to determine appropriate components for a single page.
By keeping the authorisation check as a helper object, we can use it mutliple times within a controller action, passing different parameters each time, and with the flexibility to control the outcome of each check.
For example, we could return NotFound
for one level of authorisation failure, but Unauthorized
for another:
def index = Authenticated(parse.json) { request =>
Authorised(request, request.user) {
Authorised(request, Some(Json.obj())) {
Ok(request.user.get)
} otherwise {
Unauthorized
}
} otherwise {
NotFound
}
}
Summary
We’ve now created an authentication and authorisation layer for our web application, which supports the same syntax as the built in Play objects. For a simple application, that’s all you need - just wire in MongoDB or OAuth2!
The same idea can be applied to any other type of wrapper. You can even create action builders and helper objects which combine other wrappers, for example automatically applying authentication, authorisation and rate limiting through a single builder.
Going a bit further with Akka
While it would be easy to extend that example to lookup users, roles and permissions in MongoDB, or restrict actions based on IP address, in Scala (and Play Framework) we can do things a little differently.
There’s no reason for authentication or authorisation to be tied to your application - they aren’t really a direct responsibility of your web application anyway, but they’re often left there for convenience.
Akka provides us with a framework to build distributed and concurrent applications - and we can keep our application concurrent and distributed right through to the authentication and authorisation layers. And better yet, it’s already used internally by Play Framework.
We’ll extend this example to use an actor for authentication and authorisation, giving us the flexibility to move our authentication code anywhere - even to a remote service with Akka remoting.
Creating an actor
For now, we’ll just keep our actor next to our action builder. Moving it around later is easy.
First we’ll create some basic case classes to communicate using Akka:
case class Authenticate[A](request: Request[A])
case class AuthenticationResult(valid: Boolean, user: Option[JsObject] = None)
We can extend these classes if we want to provide additional context or return additional information.
Here’s our basic actor - it implements a receive function to handle incoming messages:
class Authenticator extends Actor {
def receive = {
case Authenticate(request) =>
if(request.headers.get("Authorization").isDefined)
sender ! AuthenticationResult(valid = true, user = Json.obj())
else
sender ! AuthenticationResult(valid = false)
}
}
It’s only job is to return an AuthenticationResult depending on the request. It provides the same super strength foolproof security we had with our earlier example.
Using the actor for authentication
We need to get an instance of our actor for our Authenticated
object. We’ll send Authenticate
requests to the actor, and get an AuthenticationResult
back:
val authenticationActor = Akka.system.actorOf(Props[Authenticator], name = "authentication")
implicit val timeout = Timeout(1 second)
We can now update our invokeBlock
function to use the actor instead of including its own authentication logic. If authentication was successful, the actor returns the JsObject
representing the user.
def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[SimpleResult]) = {
(authenticationActor ask Authenticate(request)).mapTo[AuthenticationResult] flatMap { result =>
if(result.valid)
block(AuthenticatedRequest[A](Some(result.user.get), request))
else
block(AuthenticatedRequest[A](None, request))
} recover {
case e => Results.Status(Status.INTERNAL_SERVER_ERROR)
}
}
We can extend our authorisation class in exactly the same way, again sending the authorisation request to an actor. I’ll skip it here since the code is so similar to the authentication actor, but you can find it in the full source code on GitHub.
Summary
Action composition in Play Framework is surprisingly easy, and very powerful.
Using action builders combined with Scala’s concurrency and Akka’s distributed framework, it’s simple to keep your controller code clean without sacrificing security or scalability across your application.
Full source code for the Akka example can be found on GitHub.