Playframework scala how to asynchronously convert a web service call in response without Async

I am creating a web-based gaming application that connects to Google Drive. Looking through the Google OAuth 2.0 process when a user logs in, I save the access_token in Cache and save the refresh_token (along with other user data) to the database and cache. Google OAuth accessTokens for only 1 hour , and access to the access window in my cache expires in an hour.

So, as follows, I created the Authenticated function in accordance with Another way to create an authenticated action , except that in addition to the user, I also save the accessToken.

However, the accessToken expires in an hour, and if it expired, then I need to do a web service GET request for google with my refresh_token to get another access_token.

I managed to create a synchronous version that seems a little ugly but works. I was wondering if there could be a way to recycle it synchronously?

def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = { Action(p) { request => val result1 = for { userId <- request.session.get(username) user <- Cache.getAs[User](s"user$userId") token <- Cache.getAs[String](accessTokenKey(userId)) } yield f(AuthenticatedRequest(user, token, request)) import scala.concurrent.duration._ lazy val result2 = for { userId <- request.session.get(username) user <- Cache.getAs[User](s"user$userId") token <- persistAccessToken(Await.result(requestNewAccessToken(user.refreshToken)(userId), 10.seconds))(userId) } yield f(AuthenticatedRequest(user, token, request)) result1.getOrElse(result2.getOrElse(Results.Redirect(routes.Application.index()))) } } 

requestNewAccessToken makes a request to send WS to Google, sends a refreshToken along with other material, and in response to google sends back a new access token, here is the method:

 def refreshTokenBody(refreshToken: String) = Map( "refresh_token" -> Seq(refreshToken), "client_id" -> Seq(clientId), "client_secret" -> Seq(clientSecret), "grant_type" -> Seq(tokenGrantType) ) def requestNewAccessToken(refreshToken: String)(implicit userId: String): Future[Response] = WS.url(tokenUri).withHeaders(tokenHeader).post(refreshTokenBody(refreshToken)) 

It seems the only way to convert the future [ws.Response] to ws.Response is to use onComplete, but this is a callback function with a return type of Unit, which does not seem to fit the example presented in the Playframework docs (above), and I don't see how can I convert AsyncResult back to a response without redirecting it to a second router. Another possibility that I thought of is a filter that intercepts requests, and if the cache expires, just to get another one from Google and store it in the cache before the action method starts (thus, the accessToken will always be current )

As I said, the synchronous version works, and if this is the only way to implement this procedure, then so be it, but I was hoping there might be a way to do this asynchronously.

Thank you very much!

UPDATE FOR Play 2.2.0

async {} deprecated for Play 2.2.0 and will be uninstalled in Play 2.3. Therefore, the solution listed above needs to be reviewed if you are using the current version of Play.

As a reminder of the logic, when a user logs in successfully, a Google passkey is stored in the cache. Access_token only lasts an hour, so we remove access_token from the cache after an hour.

So, the Authenticated logic is that it checks to see if the userId cookie is in the request. He then uses this userId to get the User mapping from the cache. User contains refresh_token if current access_token expired. If there is no cookie userId in the cache, or we cannot get the corresponding User from Cache, then we start a new session and redirect to the destination page of the application.

If the user is successfully retrieved from the cache, we are trying to get access_token from the cache. If it is, we create a WrappedRequest object containing request , User and access_token . If this is not in the cache, we request the Google web service to get a new access_token , which is stored in the cache and then passed to WrappedRequest

To make an asynchronous request using Authenticated , just add .apply (same as for Action ), for example:

 def testing123 = Authenticated.async { Future.successful { Ok("testing 123") } } 

And here is the updated feature that works for Play 2.2.0:

 import controllers.routes import models.User import play.api.cache.Cache import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.mvc._ import play.api.Play.current import scala.concurrent.Future trait Authenticate extends GoogleOAuth { case class AuthenticatedRequest[A](user: User, accessToken: String, request: Request[A]) extends WrappedRequest[A](request) val startOver: Future[SimpleResult] = Future { Results.Redirect(routes.Application.index()).withNewSession } object Authenticated extends ActionBuilder[AuthenticatedRequest] { def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A] => Future[SimpleResult])) = { request.session.get(userName).map { implicit userId => Cache.getAs[User](userKey).map { user => Cache.getAs[String](accessTokenKey).map { accessToken => block(AuthenticatedRequest(user, accessToken, request)) }.getOrElse { // user accessToken has expired, so do WS call to Google for another one requestNewAccessToken(user.token).flatMap { response => persistAccessToken(response).map { accessToken => block(AuthenticatedRequest(user, accessToken, request)) }.getOrElse(startOver) } } }.getOrElse(startOver) // user not found in Cache }.getOrElse(startOver) // userName not found in session } } } 
+4
source share
2 answers

If you look at http://www.playframework.com/documentation/2.1.3/ScalaAsync you are invited to use the Async method. When you look at the signature, you will see how magic works:

 def Async(promise : scala.concurrent.Future[play.api.mvc.Result]) : play.api.mvc.AsyncResult 

The method returns AsyncResult, which is a subclass of Result. This means that we need to do our job of creating our usual result in the future. Then we can transfer the future result to this method, return it to our action method, and Play will take care of everyone else.

 def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = { request => { case class UserPair(userId: String, user: User) val userPair: Option[UserPair] = for { userId <- request.session.get(username) user <- Cache.getAs[User](s"user$userId") } yield UserPair(userId, user) userPair.map { pair => Cache.getAs[String](accessTokenKey(pair.userId)) match { case Some(token) => f(AuthenticatedRequest(pair.user, token, request)) case None => { val futureResponse = requestNewAccessToken(pair.user.refreshToken)(pair.userId) Async { futureResponse.map {response => persistAccessToken(response)(pair.userId) match { case Some(token) => f(AuthenticatedRequest(pair.user, token, request)) case None => Results.Redirect(routes.Application.index()) } } } } } }.getOrElse(Results.Redirect(routes.Application.index())) } } 
+3
source

Ok, I present two answers, I have to give credit to @ Karl, because (although his answer did not compile), he pointed me in the right direction:

Here is the version that breaks the process into pieces:

  def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = { Action(p) { request => { val userTuple: Option[(String, User)] = for { userId <- request.session.get(userName) user <- Cache.getAs[User](userKey(userId)) } yield (userId, user) val result: Option[Result] = for { (userId, user) <- userTuple accessToken <- Cache.getAs[String](accessTokenKey(userId)) } yield f(AuthenticatedRequest(user, accessToken, request)) lazy val asyncResult: Option[AsyncResult] = userTuple map { tuple => val futureResponse = requestNewAccessToken(tuple._2.token)(tuple._1) AsyncResult { futureResponse.map { response => persistAccessToken(response)(tuple._1).map {accessToken => f(AuthenticatedRequest(tuple._2, accessToken, request)) }.getOrElse { Results.Redirect(routes.Application.index()).withNewSession } } } } result getOrElse asyncResult.getOrElse { Results.Redirect(routes.Application.index()).withNewSession } } } 

The second option is to put everything together as one big card / card.

  def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = { Action(p) { request => { val result = request.session.get(userName).flatMap { implicit userId => Cache.getAs[User](userKey).map { user => Cache.getAs[String](accessTokenKey).map { accessToken => f(AuthenticatedRequest(user, accessToken, request)) }.getOrElse { val futureResponse: Future[ws.Response] = requestNewAccessToken(user.token) AsyncResult { futureResponse.map { response => persistAccessToken(response).map { accessToken => f(AuthenticatedRequest(user, accessToken, request)) }.getOrElse { Results.Redirect(routes.Application.index()).withNewSession}} } } } } result getOrElse Results.Redirect(routes.Application.index()).withNewSession }}} 

I have a slight preference for the second version because it allows me to use userId as implicit. I would prefer not to duplicate the redirect to index () twice, but AsyncResult will not allow this.

persistAccessToken() returns a Future[Option[String]] , so if I try to display it outside of AsyncResult , it gives me Option[String] (which makes sense if you look at the Future as a container), therefore, it should have added AsyncResult , which means that I have to provide getOrElse in case of persistAccessToken (which saves the access token in Cache and returns a copy for use) ... but that means I need two redirects in the code.

If someone knows the best way to do this, I would like to see him.

+1
source

Source: https://habr.com/ru/post/1502175/


All Articles