Jim B.
Jim B.

Reputation: 4704

Play2.2.x, BodyParser, Authentication, and Future[Result]

I'm trying to implement authentication in my Play 2.2.1 app, and I can't quite figure out how to make it work with an action that returns a Future[Result].

This post describes pretty close to what I'm trying to do, except without returning Future[Result]:

Play 2.0 Framework, using a BodyParser with an authenticated request

How can I get it to work with Futures? I.e. how would I implement this function:

def IsAuthenticated(f: => String => Request[Any] => Future[Result])

or, better yet, this function:

def IsAuthenticated[A}(b:BodyParser[A])(f: => String => Request[Any] => Future[Result])

which would feed into this function:

def AuthenticatedUser(g: Account => Request[AnyContent] => SimpleResult) = IsAuthenticated {...}

to wrap asynchronous actions in my controllers?

This part I can do:

  def IsAuthenticated(f: => String => Request[AnyContent] => Future[SimpleResult]) = {
    Security.Authenticated(email, onUnauthorized) {
      user => Action.async(request => f(user)(request))
    }
  }

But if I try to use IsAuthenticated in my wrapper function:

  def AuthenticatedUser(g: Account => Request[AnyContent] => Future[SimpleResult]) =     IsAuthenticated {
    email => implicit request => Account.find(email).map {
      opt => opt match {
        case Some(account) => g(account)(request)
        case None => Future(onUnauthorized(request))
      }
    }
  }

(Account.find returns a Future[Option[Account]] 'cause it's a mongodb call that may take some time. The desire to do the future thing right is what's causing me so much grief now)

I can't get AuthenticatedUser to satisfy the compiler. It says it's getting a Future[Future[SimpleResult]] instead of a Future[SimpleResult].

So, how best to build this whole thing? I need to be able to make authentication wrappers that rely on db calls that are asynchronous.

I'm sure I'm just dense and missing something obvious...

EDIT: Here's what I ended up with. Thank you Jean for pointing me in the right direction.

I found AuthenticatedController while rooting around and it's pretty close to what I'm looking for. I wanted two types of authentication: User (authenticated user) and Administrator (to wrap code for admin tasks).

package controllers

import models.Account

import play.api.mvc._
import scala.concurrent.Future

trait Secured {

  class AuthenticatedRequest[A](val account: Account, request: Request[A]) extends WrappedRequest[A](request)

  object User extends ActionBuilder[AuthenticatedRequest] {
    def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[SimpleResult]) = {
      request.session.get("email") match {
        case Some(email) => {
          Account.find(email).flatMap {
            case Some(account) => {
              block(new AuthenticatedRequest(account, request))
            }
            case _ => Future(Results.Redirect(routes.Index.index()))
          }
        }
        case _ => Future(Results.Redirect(routes.Index.index()))
      }
    }
  }

  object Administrator extends ActionBuilder[AuthenticatedRequest] {
    def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[SimpleResult]) = {
      request.session.get("email") match {
        case Some(email) => {
          Account.find(email).flatMap {
            case Some(account) => if (account.admin) {
              block(new AuthenticatedRequest(account, request))
            } else {
              Future(Results.Redirect(routes.Index.index()))
            }
            case _ => Future(Results.Redirect(routes.Index.index()))
          }
        }
        case _ => Future(Results.Redirect(routes.Index.index()))
      }
    }
  }
}

Upvotes: 2

Views: 1159

Answers (1)

Jean
Jean

Reputation: 21595

There have been changes in play 2.2 to make it easier to compose actions. The resource you are referring to is outdated.

Instead you should create a custom action builder by extending ActionBuilder to create your action, this will get you all the fancy apply methods you may need (including async support and all)

For example you may do :

trait MyAction extends Results{

  class MyActionBuilder[A] extends ActionBuilder[({ type R[A] = Request[A] })#R] {
    def invokeBlock[A](request: Request[A], 
                       block: Request[A] => Future[SimpleResult]) ={
      // your authentication code goes here :
      request.cookies.get("loggedIn").map { _=>
        block(request)
      } getOrElse Future.successful(Unauthorized)
    }
  }

  object MyAction extends MyActionBuilder
}

which you can then use as such :

object MyController extends Controller with MyAction{
  def authenticatedAction=MyAction {
   Ok
  }
  def asyncAuthenticatedAction=MyAction.async {
    Future.successful(Ok)
  }
  def authenticatedActionWithBodyParser = MyAction(parse.json){ request =>
    Ok(request.body)
  }
}

For brevity's sake I used a very trivial authentication mechanism you will want to change that :)

Additionally, you can create a custom "request" type to provide additional information. For instance you could define a AuthenticatedRequest as such :

  case class AuthenticatedRequest[A](user: User, request: Request[A]) extends WrappedRequest(request)

Provided you have a way to get your user such as

  object User{
    def find(s:String): Option[User] = ???
  }

Then change your builder definition a bit as such

  class MyActionBuilder[A] extends 
        ActionBuilder[({ type R[A] = AuthenticatedRequest[A] })#R] {
    def invokeBlock[A](request: Request[A], 
                       block: AuthenticatedRequest[A] => Future[SimpleResult]) ={
      // your authentication code goes here :
      (for{
        userId <- request.cookies.get("userId")
        user <- User.find(userId.value)
      }yield {
        block(AuthenticatedRequest(user,request))
      }) getOrElse Future.successful(Unauthorized)
    }
  }

Your controller now has access to your user in authenticatedActions:

object MyController extends Controller with MyAction{
  val logger = Logger("application.controllers.MyController")
  def authenticatedAction=MyAction { authenticatedRequest =>
    val user = authenticatedRequest.user
   logger.info(s"User(${user.id} is accessing the authenticatedAction")  
   Ok(user.id)
  }
  def asyncAuthenticatedAction = MyAction.async { authenticatedRequest=>
    Future.successful(Ok(authenticatedRequest.user.id))
  }
  def authenticatedActionWithBodyParser = MyAction(parse.json){ authenticatedRequest =>
    Ok(authenticatedRequest.body)
  }
}

Upvotes: 4

Related Questions