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

后端 未结 5 677
执笔经年
执笔经年 2021-02-01 03:14

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 see

相关标签:
5条回答
  • 2021-02-01 03:40

    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))
    }
    
    0 讨论(0)
  • 2021-02-01 03:49

    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 ...)
          }
        }
      )
    }
    
    0 讨论(0)
  • 2021-02-01 03:50

    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.")
          }
        }
      }
    }
    
    0 讨论(0)
  • 2021-02-01 03:50

    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.

    0 讨论(0)
  • 2021-02-01 04:05

    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.

    0 讨论(0)
提交回复
热议问题