Dao De Code

Testing Security.Authenticated in Play 2.0 and 2.1

You have a Play application and let's say you want to add some simple authentication logic to restrict access to your actions. You want all your users to provide a ticket (e.g cas proxy ticket) in a request header, so you can use the ticket to authenticate them.

Play's Security.Authenticated to the rescue!

You can come up with something like this in Play 2.1:

trait Secured {

  val logger = Logger("secured")

  final def fail(reason: String) = {
    logger.debug(s"Access attempt failed: $reason")
    Unauthorized("must be authenticated")
  }

  final def secured[A](action: Action[A]) =
    Security.Authenticated(
      req => req.headers.get("authTicket"),
      _ => fail("no ticket found")) {
      ticket => Action(action.parser) {
        request => withTicket(ticket) {
          action(request)
        }
      }
    }

  private def withTicket(ticket: String)(produceResult: => Result): Result =
    Async {
      isValid(ticket) map {
        valid => if (valid) produceResult else fail(s"provided ticket $ticket is invalid")
      }
    }

  def isValid(ticket: String): Future[Boolean]
}

In Play 2.0 it looks almost same except that isValid returns old play's Promise instead of scala's Future.

And you can use it like this:

    def securedAction = secured {
      Action {
        request => Ok("Am I protected?")
      }
    }

All good, but how do we know it works? Test it!

Let's create a fake controller which has both secured and non-secured actions. And we'll use it in our tests, or rather specs.

  object FakeController extends Controller with Secured {
    def securedAction = secured {
      Action {
        request => Ok("Am I protected?")
      }
    }

    def nonSecuredAction = Action {
      request => Ok("I don't care")
    }

    def isValid(ticket: String) = Promise.pure(ticket == "valid")
  }

To test that authTicket param is required we can have something like this:

    "return UNAUTHORIZED if `authTicket` param is not provided" in {
      running(app) {
        val result = FakeController.securedAction process FakeRequest()
        status(result) must_== UNAUTHORIZED
        contentAsString(result) must_== "must be authenticated"
      }
    }

Here app is a FakeApplication and process is our method (added through implicits) which returns a Result given a Request.

Please note in Play 2.0 status and contentAsString don't know how to handle AsyncResults, so with this implementation of withTicket you need to do some trible dance to make it work.

In Play 2.0 Authenticated returnes Action[(Action[A], A)] so our process looks like this:

  implicit def action2actionExecutor[A](wrapped: Action[(Action[A], A)]): ActionExecutor[A]
  = new ActionExecutor[A](wrapped)

  class ActionExecutor[A](wrapped: Action[(Action[A], A)]) {
    def process(request: Request[A]): Result = wrapped.parser(request).run.await.get match {
      case Left(errorResult) => errorResult
      case Right((innerAction, _)) => innerAction(request)
    }
  }

in Play 2.1 we get just EssentialAction (and we can use implicit class) so it looks much simple:

  implicit class ActionExecutor(action: EssentialAction) {
    def process[A](request: Request[A]): Result =
      concurrent.Await.result(action(request).run, Duration(1, "sec"))
  }

To test what happens if authTicket is provided but is not valid we can do the following:

    def requestWithAuthTicket(ticket: String = "invalid") =
      FakeRequest().withHeaders("authTicket" -> ticket)

    "return UNAUTHORIZED if `authTicket` param is not valid" in {
      running(app) {
        val result = FakeController.securedAction process requestWithAuthTicket()
        status(result) must_== UNAUTHORIZED
        contentAsString(result) must_== "must be authenticated"
      }
    }

To make sure that it allows access when ticket is valid let's add this

    "return whatever it returns if `authTicket` param is valid" in {
      running(app) {
        val result = FakeController.securedAction process requestWithAuthTicket(ticket = "valid")
        status(result) must_== OK
        contentAsString(result) must_== "Am I protected?"
      }
    }

Full source code can be found on github in branches play20 and play21 correspondingly.

comments powered by Disqus