Play framework and Authorization
Recently, I need to write a authorization solution for play framework. From my understanding, it is just a filter to protect the resources of all my actions in controllers.
I have done this for Spary framework. This time, we are facing play framework. It takes me sometime to figure this out.
First of all, I am using play framework 2.4.3 version.
I am using configuration file, but actually we should use Redis or other storage. But I only want to show how the filter works among the actions and controllers.
apigateway {
mapping=[
{ "key1" : "campaignId1" }
{ "key1" : "campaignId2" }
]
}
Here is how I read this configuration map.
val api_gateway_mapping: Map[String, Seq[String]]= {
val list:Iterable[ConfigObject] = config.getObjectList("apigateway.mapping").asScala
val result = (
for {
item:ConfigObject <- list
entry : Entry[String, ConfigValue] <- item.entrySet().asScala
key = entry.getKey
value = entry.getValue.unwrapped().toString
} yield (key, value)).groupBy(record => record._1 ) map { item =>
item._1 -> {
item._2.toSeq map { value =>
value._2
}
}
}
result
}
2 Important models
package com.sillycat.services.auth
import play.api.mvc.WrappedRequest
class ResourceRequest[A](val campaigns: Seq[String], val campaignID:String, request: TokenRequest[A]) extends WrappedRequest[A](request) {
def token = request.token
}
package com.sillycat.services.auth
import play.api.mvc._
class TokenRequest[A](val token: Option[String], request: Request[A]) extends WrappedRequest[A](request)
These requests are hosting more parameters, like token, campaignID, resources and etc.
Actions to Auth
what we will do in these actions, first, get the token from headers ——> fetch the mapping based on the token ——> get the resources from that request ——> match the mapping with the resources from request
#1 Get token from Headers
package com.sillycat.services.auth
import com.sillycat.utils.IncludeLogger
import play.api.mvc.{Request, ActionTransformer, ActionBuilder}
import scala.concurrent.Future
object TokenFetchAction
extends ActionBuilder[TokenRequest]
with ActionTransformer[Request, TokenRequest]
with IncludeLogger
{
def transform[A](request: Request[A]) = Future.successful {
val xToken = request.headers.get("x-api-key")
logger.trace("TokenFetchAction system gets token = " + xToken)
new TokenRequest(xToken, request)
}
}
#2 Fetch the Mapping
package com.sillycat.services.auth
object AuthorizeService
extends IncludeAuthConfig
{
/**
* no guava cache, no expire time, this is a just temperary solution
*/
val memoryCache:Map[String, Seq[String]] = api_gateway_mapping
def getResourceMappingByToken(xToken:String):Option[Seq[String]] = {
val resources = memoryCache.get(xToken)
resources
}
}
#3 Get resources from requests
package com.sillycat.services.auth
import com.sillycat.utils.IncludeLogger
import play.api.mvc.{Results, ActionRefiner}
import scala.concurrent.Future
trait IncludeAuthService
extends IncludeLogger
{
def ResourceFetchAction(campaignID:String) = new ActionRefiner[TokenRequest, ResourceRequest] {
def refine[A](request: TokenRequest[A]) = Future.successful {
val xTokenOpt = request.token
logger.trace("ResourceFetchAction get xTokenOpt = " + xTokenOpt)
xTokenOpt match {
case Some(xToken) => {
logger.trace("ResourceFetchAction get xToken = " + xToken)
val resourceOpt = AuthorizeService.getResourceMappingByToken(xToken)
resourceOpt.map{ resources =>
logger.trace("ResourceFetchAction get resources mapping = " + resources)
logger.trace("ResourceFetchAction get campaignID = " + campaignID)
new ResourceRequest(resources, campaignID, request)
}.toRight{
logger.error("ResourceFetchAction empty resources mapping data resourceOpt = " + resourceOpt)
Results.Unauthorized
}
}
case _ => {
logger.error("System does not get x-api-key, bad request.")
Left(Results.Unauthorized)
}
}
}
}
}
#4 Match the Mapping with resources
package com.sillycat.services.auth
import com.sillycat.utils.IncludeLogger
import play.api.mvc.{Results, ActionFilter}
import scala.concurrent.Future
object AuthCampaignCheckAction
extends ActionFilter[ResourceRequest]
with IncludeLogger
{
def filter[A](request: ResourceRequest[A]) = Future.successful {
val campaignID = request.campaignID
val resources = request.campaigns
logger.trace("AuthCampaignCheckAction campaignID = " + campaignID)
logger.trace("AuthCampaignCheckAction resources = " + resources)
if (resources.contains(campaignID)){
logger.trace("AuthCampaignCheckAction, You are authorized!")
None
} else{
logger.warn("AuthCampaignCheckAction, Unauthorized!")
Some(Results.Unauthorized)
}
}
}
After that, when we wants to use them in the Controller.
…snip...
with IncludeAuthService
..snip...
def addJob(
@ApiParam(name="campaignID",value="campaign ID",defaultValue=“xxxxx",required=true,allowMultiple=false)
@PathParam("campaignID")
campaignID:String
) = (TokenFetchAction andThen ResourceFetchAction(campaignID) andThen AuthCampaignCheckAction)(BodyParsers.parse.json) { implicit request =>
…snip...
References:
https://www.playframework.com/documentation/2.4.x/ScalaActionsComposition
http://stackoverflow.com/questions/19868153/authorisation-check-in-controller-scala-play