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 } } }