I've a method that returns a Future
like this...
def isTokenExpired(token: String): Future[Boolean] = {
...
}
... and then I've another method that invokes isTokenExpired
that returns a Boolean
like this:
def isExpired(token: String): Boolean = {
var result = true
isTokenExpired(token).onComplete {
case Success(r) => result = r
case Failure(_) => result = true
}
result
}
Is there a better way to write the isExpired
method?
EDIT
As requested by EECOLOR, let me provide you with more details. For my Play application I've implemented an authorization mechanism based on JSON Web Token (jwt). All the claims are contained in the jwt except the expiration time, which is stored in a MongoDB collection. Here below is a summary of how my Token
class looks like:
class Token {
...
def id: String = { ... }
def issueTime: LocalDateTime = { ... }
def issuer: String = { ... }
...
def isValid: Boolean = { ... }
def isExpired: Boolean = { /* uses ReactiveMongo to access MongoDB */ }
}
As you can see, all the jwt properties are self-contained except expiration info. Method isExpired
uses ReactiveMongo, which always returns a Future
. To make things even more complex, I use this jwt in a customized Action
like this:
class SecuredAction[T <: Controller] private(private val methodName: String)
extends ActionBuilder[ApiRequest] {
...
def invokeBlock[A](request: Request[A], block: (ApiRequest[A]) => Future[SimpleResult]) = {{
request.headers.get(HeaderNames.AUTHORIZATION) match {
case Some(header) => s"""$AuthType (.*)""".r.unapplySeq(header).map(_.head.trim)
case _ => None
}} match {
case Some(tokenString) => {
val token = Token(tokenString)
if (!token.isValid) {
Logger.warn(s"request ${request.uri} not authorized: token ${token.id} has been tampered")
Future.successful(Unauthorized(AuthErrors.authenticationViolated(token.subject)(request).asJson))
} else if (token.isExpired) {
Logger.debug(s"request ${request.uri} not authorized: token ${token.id} has expired")
Future.successful(Unauthorized(AuthErrors.authenticationExpired(token.subject)(request).asJson))
} else if (!isAuthorized(token)) {
Logger.info(s"request ${request.uri} not authorized: required claims not defined for account ${token.subject}")
Future.successful(Forbidden(AuthErrors.requestNotAuthorized(token.subject)(request).asJson))
} else {
Logger.debug(s"request ${request.uri} authorized for account ${token.subject}")
block(new ApiRequest(token, request))
}
}
case _ => {
Logger.debug(s"request ${request.uri} not authenticated")
Future.successful(Unauthorized(
AuthErrors.requestNotAuthenticated()(request).asJson
).withHeaders(HeaderNames.WWW_AUTHENTICATE -> AuthType))
}
}
}
As you can see, I need to return a Future[play.mvc.results.Result]
, not a Future[Boolean]
as would return isExpired
if I used Future.map
. Do you get to point?
Future represents a result of an asynchronous computation that may or may not be available yet. When we create a new Future, Scala spawns a new thread and executes its code. Once the execution is finished, the result of the computation (value or exception) will be assigned to the Future.
sequence takes a list of futures and transforms it into a single future of list in an asynchronous manner. For instance, assume that you have a list of independent jobs to be run simultaneously. In such a case, the list of futures can be composed into a single future of list using Future.
Await. result tries to return the Future result as soon as possible and throws an exception if the Future fails with an exception while Await. ready returns the completed Future from which the result (Success or Failure) can safely be extracted.
The function you wrote will not work as you would think. It would (likely) first return true
and later set the result
variable.
Normally you would do something like this:
isTokenExpired(token).map { result =>
// do stuff
}
In a framework like Play you would map the Future
to an http response and give Play back a Future[SimpleResult]
. Play knows how to handle Future
results.
In general it's recommended you do not wait for a Future
to complete in production code, but work with the values in the Future
and let the framework you are using handle the result.
In tests it might come in handy to wait for a result, you can do that like this:
Await.result(someFuture, 5.seconds)
Edit
I would probably extract the construction of a token so that I end up with a Future[Token]
. That allows me to more easily compose things. It also allows me to create code that has a better architecture and is easier to test.
I would probably break down the code into more smaller methods, but the example below gives you an idea of the direction I would take.
class TokenService(connection: MongoConnection) {
def tokenFor(tokenString: String): Future[Token] = ???
}
class SecuredAction(tokenService: TokenService) extends
ActionBuilder[ApiRequest] {
import play.api.libs.concurrent.Execution.Implicits._
def invokeBlock[A](request: Request[A], block: (ApiRequest[A]) => Future[SimpleResult]) =
extractTokenFrom(request) match {
case Some(tokenString) => {
tokenService.tokenFor(tokenString) flatMap {
case token if (!token.isValid) =>
Logger.warn(s"request ${request.uri} not authorized: token ${token.id} has been tampered")
Future.successful(Unauthorized(AuthErrors.authenticationViolated(token.subject)(request).asJson))
case token if (token.isExpired) =>
Logger.debug(s"request ${request.uri} not authorized: token ${token.id} has expired")
Future.successful(Unauthorized(AuthErrors.authenticationExpired(token.subject)(request).asJson))
case token if (!token.isAuthorized) =>
Logger.info(s"request ${request.uri} not authorized: required claims not defined for account ${token.subject}")
Future.successful(Forbidden(AuthErrors.requestNotAuthorized(token.subject)(request).asJson))
case token =>
Logger.debug(s"request ${request.uri} authorized for account ${token.subject}")
block(new ApiRequest(token, request))
}
}
case _ =>
Logger.debug(s"request ${request.uri} not authenticated")
Future.successful(Unauthorized(
AuthErrors.requestNotAuthenticated()(request).asJson).withHeaders(HeaderNames.WWW_AUTHENTICATE -> AuthType))
}
val AuthType = "MyAuthType"
val TokenHeader = s"""$AuthType (.*)""".r
def extractTokenFrom(request: RequestHeader) = {
val authorizationHeader = request.headers.get(HeaderNames.AUTHORIZATION)
authorizationHeader flatMap {
case TokenHeader(token) => Some(token.trim)
case _ => None
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With