Can I do async form validation in Play Framework 2.x (Scala)?
Asked Answered
M

5

30

I'm making a real push to understand the async powers of Play but finding a lot of conflict with regard to places where async invocation fits and places where the framework seems to conspire against its use.

The example I have relates to form validation. Play allows for ad-hoc constraints to be defined - see this from the docs:

val loginForm = Form(
  tuple(
    "email" -> email,
    "password" -> text
  ) verifying("Invalid user name or password", fields => fields match { 
      case (e, p) => User.authenticate(e,p).isDefined 
  })
)

Nice and clean. However, if I'm using a fully async data access layer (e.g. ReactiveMongo), such a call to User.authenticate(...) would return a Future and I'm thus in the dark as to how I can utilise the power of both the built in form binding features and the async tools.

It's all well and good to publicise the async approach but I'm getting frustrated that certain parts of the framework don't play so well with it. If the validation has to be done synchronously, it seems to defeat the point of the async approach. I've come across a similar problem when using Action composition - e.g. a security related Action that would make a call to ReactiveMongo.

Can anyone shed any light on where my comprehension is falling short?

Myers answered 16/2, 2013 at 17:30 Comment(0)
D
9

Yes, validation in Play is designed synchronously. I think it's because assumed that most of time there is no I/O in form validation: field values are just checked for size, length, matching against regexp, etc.

Validation is built over play.api.data.validation.Constraint that store function from validated value to ValidationResult (either Valid or Invalid, there is no place to put Future here).

/**
 * A form constraint.
 *
 * @tparam T type of values handled by this constraint
 * @param name the constraint name, to be displayed to final user
 * @param args the message arguments, to format the constraint name
 * @param f the validation function
 */
case class Constraint[-T](name: Option[String], args: Seq[Any])(f: (T => ValidationResult)) {

  /**
   * Run the constraint validation.
   *
   * @param t the value to validate
   * @return the validation result
   */
  def apply(t: T): ValidationResult = f(t)
}

verifying just adds another constraint with user-defined function.

So I think Data Binding in Play just isn't designed for doing I/O while validation. Making it asynchronous would make it more complex and harder to use, so it kept simple. Making every piece of code in framework to work on data wrapped in Futures is overkill.

If you need to use validation with ReactiveMongo, you can use Await.result. ReactiveMongo returns Futures everywhere, and you can block until completion of these Futures to get result inside verifying function. Yes, it will waste a thread while MongoDB query runs.

object Application extends Controller {
  def checkUser(e:String, p:String):Boolean = {
    // ... construct cursor, etc
    val result = cursor.toList().map( _.length != 0)

    Await.result(result, 5 seconds)
  }

  val loginForm = Form(
    tuple(
      "email" -> email,
      "password" -> text
    ) verifying("Invalid user name or password", fields => fields match { 
      case (e, p) => checkUser(e, p)
    })
  )

  def index = Action { implicit request =>
    if (loginForm.bindFromRequest.hasErrors) 
      Ok("Invalid user name")
    else
      Ok("Login ok")
  }
}

Maybe there's way to not waste thread by using continuations, not tried it.

I think it's good to discuss this in Play mailing list, maybe many people want to do asynchronous I/O in Play data binding (for example, for checking values against database), so someone may implement it for future versions of Play.

Detent answered 18/2, 2013 at 7:38 Comment(1)
How can I set validation message dynamicly? Message may be "Invalid user name or password" or "Service unavailable now" for example. And the second question is can I get User object in Action without duplicate auth-request?Diviner
R
6

I've been struggling with this, too. Realistic applications are usually going to have some sort of user accounts and authentication. Instead of blocking the thread, an alternative would be to get the parameters out of the form and handle the authentication call in the controller method itself, something like this:

def authenticate = Action { implicit request =>
  Async {
    val (username, password) = loginForm.bindFromRequest.get
    User.authenticate(username, password).map { user =>
      user match {
        case Some(u: User) => Redirect(routes.Application.index).withSession("username" -> username)
        case None => Redirect(routes.Application.login).withNewSession.flashing("Login Failed" -> "Invalid username or password.")
      }
    }
  }
}
Roubaix answered 4/5, 2013 at 16:23 Comment(0)
W
3

Form validation means syntactic validation of fields, one by one. If a filed does not pass the validation it can be marked (eg. red bar with message).

Authentication should be placed in the body of the action, which may be in an Async block. It should be after the bindFromRequest call, so there must me after the validation, so after each field is not empty, etc.

Based on the result of the async calls (eg. ReactiveMongo calls) the result of the action can be either BadRequest or Ok.

Both with BadRequest and Ok can redisplay the form with error message if the authentication failed. These helpers only specify the HTTP status code of the response, independently to the response body.

It would be an elegant solution to do the Authentication with play.api.mvc.Security.Authenticated (or write a similar, customized action compositor), and use Flash scoped messages. Thus the user always would be redirected to login page if she is not authenticated, but if she submits the login form with wrong credentials the error message would be shown besides the redirect.

Please take a look on the ZenTasks example of your play installation.

Wertz answered 3/6, 2013 at 19:45 Comment(0)
G
2

The same question was asked in the Play mailing list with Johan Andrén replying:

I'd move the actual authentication out of the form validation and do it in your action instead and use the validation only for validation of required fields etc. Something like this:

val loginForm = Form(
  tuple(
    "email" -> email,
    "password" -> text
  )
)

def authenticate = Action { implicit request =>
  loginForm.bindFromRequest.fold(
    formWithErrors => BadRequest(html.login(formWithErrors)),
    auth => Async {
      User.authenticate(auth._1, auth._2).map { maybeUser =>
        maybeUser.map(user => gotoLoginSucceeded(user.get.id))
        .getOrElse(... failed login page ...)
      }
    }
  )
}
Germanism answered 3/2, 2015 at 22:5 Comment(0)
E
1

I've seen on theguardian's GH repo how they handle this case scenario in a asynchronous way while still having the support of the form error helpers from play. From a quick look, seems like they are storing the form errors in an encrypted cookie in a way as to display those errors back to the user the next time the user goes to the login page.

Extracted from: https://github.com/guardian/facia-tool/blob/9ec455804edbd104861117d477de9a0565776767/identity/app/controllers/ReauthenticationController.scala

def processForm = authenticatedActions.authActionWithUser.async { implicit request =>
  val idRequest = idRequestParser(request)
  val boundForm = formWithConstraints.bindFromRequest
  val verifiedReturnUrlAsOpt = returnUrlVerifier.getVerifiedReturnUrl(request)

  def onError(formWithErrors: Form[String]): Future[Result] = {
    logger.info("Invalid reauthentication form submission")
    Future.successful {
      redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)
    }
  }

  def onSuccess(password: String): Future[Result] = {
      logger.trace("reauthenticating with ID API")
      val persistent = request.user.auth match {
        case ScGuU(_, v) => v.isPersistent
        case _ => false
      }
      val auth = EmailPassword(request.user.primaryEmailAddress, password, idRequest.clientIp)
      val authResponse = api.authBrowser(auth, idRequest.trackingData, Some(persistent))

      signInService.getCookies(authResponse, persistent) map {
        case Left(errors) =>
          logger.error(errors.toString())
          logger.info(s"Reauthentication failed for user, ${errors.toString()}")
          val formWithErrors = errors.foldLeft(boundForm) { (formFold, error) =>
            val errorMessage =
              if ("Invalid email or password" == error.message) Messages("error.login")
              else error.description
            formFold.withError(error.context.getOrElse(""), errorMessage)
          }

          redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)

        case Right(responseCookies) =>
          logger.trace("Logging user in")
          SeeOther(verifiedReturnUrlAsOpt.getOrElse(returnUrlVerifier.defaultReturnUrl))
            .withCookies(responseCookies:_*)
      }
  }

  boundForm.fold[Future[Result]](onError, onSuccess)
}

def redirectToSigninPage(formWithErrors: Form[String], returnUrl: Option[String]): Result = {
  NoCache(SeeOther(routes.ReauthenticationController.renderForm(returnUrl).url).flashing(clearPassword(formWithErrors).toFlash))
}
Erskine answered 23/9, 2015 at 5:30 Comment(1)
The encryption stuff goes into the "toFlash" implicit method that can be found in their file implicits.Forms.scalaErskine

© 2022 - 2024 — McMap. All rights reserved.