Implementing authorization for API on the top of Play Framework 2.4*
The article considers the implementation of an easy to use authorization mechanism based on Play Framework 2.4* Actions architecture with full power of classical Actions
Introduction
It is a very oft-repeated task while we are creating public API, we need a mechanism for authorization of requests. Ideally, it should not depend on the type of requests, so that we can check sign for all API calls. In this article, we take a look at approaches how can we do that in Play framework, problems that can appear and we propose one of the possible solutions, that from the point of the author seems to be comfortable in use.
I like the approach of SecureSocial, and of course, we can use it. However, SecureSocail is too huge for such simple task; I do not need functionality with all oAuth and oAuth2 logic, and there is much stuff that I should implement to get it working. So we just get some approaches from there, and write own authorization with BlackJack and Friends ;)
Authorization with Blackjack and Friends
First, let's define our Authorization trait. To simplify task( we do not want to write separate authorization logic for different content types), let’s define isAuthorized
method with input parameter request, already parameterized by String - that means that we handle body only as a string: Request[String]
trait Authorization {
/**
* Checks whether the API client is authorized to execute an action or not.
*
* @return
*/
def isAuthorized(request: Request[String]): Boolean
}
Note: The other way was to parameterise request with AnyContent
, it sounds like a more general way - because we can parse such request with default body parser to any supported content type. In the case of simplicity, I decided to choose String.
Next, we define some implementation of Authorization trait; let's assume that we want to sign each request with some token constructed as “${signature}_${publicKey}”(where signature - calculated sign for request data and publicKey - public key for API):
import play.api.Logger
import play.api.mvc.{ Request}
object Authorization {
val TOKEN_PARAM = "token"
}
class TokenAuthorization(signatureCalculator: ApiSignatureCalculator, store: TokenStore) extends Authorization {
import Authorization._
val log = Logger(getClass)
/**
* Checks whether the API client is authorized to execute an action or not.
*
* @return
*/
override def isAuthorized(request: Request[String]): Boolean = {
log.info(s">>>> isAuthorized - Params : [$request]")
request.getQueryString(TOKEN_PARAM).exists { signString =>
val splittedString:List[String] = signString.split("_").toList
val sign = splittedString.headOption.getOrElse("")
val publicKey = splittedString.tail.headOption.getOrElse("")
val tokenOpt = store.token(publicKey)
log.info(s"---- isAuthorized - sign: [$sign] token: [$tokenOpt] ")
log.info(s"---- isAuthorized - request uri: ${request.uri}")
tokenOpt.exists { token =>
signatureCalculator.sign(
privateKey = token.privateKey,
method = request.method,
uri = request.uri.replaceAll("\\?.*", ""),
queryParams = request.queryString.mapValues(_.headOption).collect {
case (key, Some(value)) => (key, value)
} - TOKEN_PARAM,
body = request.body
).map(_ == sign).getOrElse(false)
}
}
}
}
Where TokenStore - store of mapping between public and private keys:
case class Token(publicKey: String, privateKey: String)
trait TokenStore {
def tokens: Seq[Token]
def token(publicKey: String): Option[Token]
}
moreover, ApiSignatureCalculator - implements the logic how to sign request data
trait ApiSignatureCalculator {
def sign(privateKey: String,
method: String,
uri: String,
queryParams: Map[String, String],
body: String):Try[String]
}
Controllers in Play Framework
In Play Framework controllers consist of methods that create Action objects to handle the incoming requests. For example in the HelloController below the helloWorld and hello methods create actions that return a “hello” message to the client. In this case, the action objects are constructed using the apply factory method of the Action singleton object.
import play.api.Logger
import play.api.mvc._
class HelloController extends Controller {
def helloWorld = Action { request =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
// …. some logic with body
Ok("Hello World")
}
def hello(name: String) = Action(parse.json) { request =>
// here request.body is already an instance of JsValue
// …. some logic with body
Ok(s"Hello $name")
}
}
Also, as you can see in the example above Play Framework has a powerful concept of Body parsers.
There are two ways of usage of Body parsers - the default body parser(see def helloWorld method) or you can specify body parser explicitly for Action (see def hello(name: String) method)So, what we need?
In order of simplicity of usage, I want to get similar to SecureSocial functionality so that I will be able just to extend from some SecuredController and use SecuredActions instead of regular Actions. Something like that:class HelloController extends SecuredController {
def helloWorld = SecuredAction { request =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
// …. some logic
Ok("Hello World")
}
def hello(name: String) = SecuredAction(parse.json) { request =>
// …. some logic
Ok(s"Hello $name")
}
}
What problems may appear?
As we have defined our Authorization trait, we need to get the body as a String(or in other implementation as AnyContent). In a case of def helloWorld method, we use default body parser. The default body parser produces a body of type AnyContent. The various types supported by AnyContent are accessible via as methods, such as asJson, which returns an Option of the body type. So we can use asText method and get access to the body as a String. In this case, everything is Okay. However, in the case of def hello(name: String) we are specifying body parser explicitly and request body is transformed to some of the representations of the ContentTypes and it is sometimes impossible(if we use custom BodyParsers) or a painful to reconstruct initial request and write for this common logic. In order to support full functionality of ordinary Actions we need somehow implement the ability to parse request body at first as String (or AnyContent) and then parse request body with explicilty defined body parser. The problem is that standard implementation of concept of BodyParser it is not allowed because all functionality is binded to the concrete BodyParser that passed to the Action. However, the sollution exists - Jürgen Strobel proposed to create DualBodyParser (or you can extend this logic - TrialBodyParser, QuadralBodyParser, etc. ) - http://stackoverflow.com/questions/19888717/how-to-access-the-body-of-a-request-as-byte-arrayDualBodyParser
A BodyParser, which executes any two provided BodyParsers in parallel.final case class DualBodyParser[+A, +B](
a: BodyParser[A],
b: BodyParser[B]
)(
implicit ec: ExecutionContext = defaultContext
)
extends BodyParser[(A, B)]
{
def apply(v1: RequestHeader): Iteratee[Array[Byte], Either[Result, (A, B)]] =
Enumeratee.zipWith(a(v1), b(v1)) {
case (Left(va), _) => Left(va)
case (_, Left(vb)) => Left(vb)
case (Right(va), Right(vb)) => Right((va, vb))
}
}
The results are combined in the following way:
If any wrapped parser's Iteratee encounters an Error, that's the result.
Else if the first parser's Iteratee finally yields a Left, this is used as the result.
Else if the second parser's Iteratee yields a Left, this is used as the result.
Else both Right results are combined in a Right[(A, B)].
This approach can be used to provide the request's body both as a RawBuffer and a JSON-parsed
custom model class, or to feed the body through an HMAC module in addition to parsing it.
SecuredRequest, SecuredActionBuilder and SecuredAction
Now let's define SecuredRequest - a wrapper around the ordinary Request and SecuredController abstract class that depends on the Authorization trait:object SecuredController {
case class SecuredRequest[A](request: Request[A]) extends WrappedRequest(request)
}
abstract class SecuredController(authorize: Authorization) extends Controller {
// …. here we will define SecuredActionBuilder and SecuredAction
}
Inside of the abstract class SecuredController body now we should define our custom SecuredActionBuilder that can handle DualBodyParser logic.
Standard implemetation of parsing request with the body parser in the trait ActionBuilder is based on the next method:
final def async[A](bodyParser: BodyParser[A])(block: R[A] => Future[Result]): Action[A] = composeAction(new Action[A] {
def parser = composeParser(bodyParser)
def apply(request: Request[A]) = try {
invokeBlock(request, block)
} catch {
// NotImplementedError is not caught by NonFatal, wrap it
case e: NotImplementedError => throw new RuntimeException(e)
// LinkageError is similarly harmless in Play Framework, since automatic reloading could easily trigger it
case e: LinkageError => throw new RuntimeException(e)
}
override def executionContext = ActionBuilder.this.executionContext
})
So now we should implement our custom SecuredActionBuilder where final def async[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[(String,A)]
method will use DualBodyParser and Authorization trait in order to correctly authorize requests, and most other methods will be based on this functionality:
class SecuredActionBuilder(authorizeOpt: Option[Authorization]) extends ActionFunction[Request, Request] {
self =>
val log = Logger(getClass)
final def async[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[(String,A)] =
composeAction(
new Action[(String, A)] {
def parser = DualBodyParser(parse.tolerantText, composeParser(bodyParser))
def apply(request: Request[(String,A)]) = try {
log.info(s"---- async - authorize: $authorizeOpt")
if (authorizeOpt.isEmpty || authorizeOpt.get.isAuthorized(request map (_._1))) {
invokeBlock(request map (_._2), block)
} else {
failUnauthorized("Not Authorized")
}
} catch {
case e: NotImplementedError => throw new RuntimeException(e)
case e: LinkageError => throw new RuntimeException(e)
}
override def executionContext = self.executionContext
}
)
// here you should define your own logic how to handle unathorized requests
def failUnauthorized(error: String): Future[Result] = Future.successful{
Ok(Json.stringify(Json.obj(
"result" -> "error",
"code" -> 401,
"message" -> error
)))
}
// Note: the result Actions now will be parameterized with tuple (String, A) - where A - content parsed with body parser
final def apply[A](bodyParser: BodyParser[A])(block: Request[A] => Result): Action[(String,A)] = async(bodyParser) { req: Request[A] =>
Future.successful(block(req))
}
final def apply(block: Request[AnyContent] => Result): Action[(String,AnyContent)] = apply(BodyParsers.parse.default)(block)
final def apply(block: => Result): Action[(String,AnyContent)] = apply(_ => block)
final def async(block: => Future[Result]): Action[(String,AnyContent)] = async(_ => block)
final def async(block: Request[AnyContent] => Future[Result]): Action[(String,AnyContent)] = async(BodyParsers.parse.default)(block)
// standard implemetation of the ActionBuilder
protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = bodyParser
protected def composeAction[A](action: Action[A]): Action[A] = action
override def andThen[Q[_]](other: ActionFunction[Request, Q]): ActionBuilder[Q] = new ActionBuilder[Q] {
def invokeBlock[A](request: Request[A], block: Q[A] => Future[Result]) =
self.invokeBlock[A](request, other.invokeBlock[A](_, block))
override protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = self.composeParser(bodyParser)
override protected def composeAction[A](action: Action[A]): Action[A] = self.composeAction(action)
}
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]): Future[Result] = block(request)
}
As you can see we are creating a new Action[(String, A)]
(parameterized with the tuple that is a result of parsing in parallel request as String and data type A that was parsed with provided explicitly body parser ) with overrided methods:
def parse method, that is now a DualBodyParser
constructed from two - tolerantText
body parser and body parser passed explicitly.
def apply(request: Request[(String, A)])
- that handles request parsed with DualBodyParser
and define authorization mehanizm.
Next, in the Inside of the abstract class SecuredController
body we should define SecuredAction
built with SecuredActionBuilder
and Authorization
trait.
object SecuredAction extends SecuredActionBuilder(Some(authorize)) {
def apply[A]() = new SecuredActionBuilder(Some(authorize))
def apply[A](authorize: Authorization) = new SecuredActionBuilder(Some(authorize))
}
As a result we will get something like this:
import play.api.Logger
import play.api.libs.iteratee.{Enumeratee, Iteratee}
import play.api.libs.json.Json
import play.api.mvc._
import play.api.libs.concurrent.Execution.defaultContext
import scala.concurrent.{ExecutionContext, Future}
object SecuredController {
case class SecuredRequest[A](request: Request[A]) extends WrappedRequest(request)
}
abstract class SecuredController(authorize: Authorization) extends Controller {
object SecuredAction extends SecuredActionBuilder(Some(authorize)) {
def apply[A]() = new SecuredActionBuilder(Some(authorize))
def apply[A](authorize: Authorization) = new SecuredActionBuilder(Some(authorize))
}
final case class DualBodyParser[+A, +B](
a: BodyParser[A],
b: BodyParser[B]
)(
implicit ec: ExecutionContext = defaultContext
)
extends BodyParser[(A, B)]
{
def apply(v1: RequestHeader): Iteratee[Array[Byte], Either[Result, (A, B)]] =
Enumeratee.zipWith(a(v1), b(v1)) {
case (Left(va), _) => Left(va)
case (_, Left(vb)) => Left(vb)
case (Right(va), Right(vb)) => Right((va, vb))
}
}
class SecuredActionBuilder(authorizeOpt: Option[Authorization]) extends ActionFunction[Request, Request] {
self =>
val log = Logger(getClass)
final def apply[A](bodyParser: BodyParser[A])(block: Request[A] => Result): Action[(String,A)] = async(bodyParser) { req: Request[A] =>
Future.successful(block(req))
}
final def apply(block: Request[AnyContent] => Result): Action[(String,AnyContent)] = apply(BodyParsers.parse.default)(block)
final def apply(block: => Result): Action[(String,AnyContent)] = apply(_ => block)
final def async(block: => Future[Result]): Action[(String,AnyContent)] = async(_ => block)
final def async(block: Request[AnyContent] => Future[Result]): Action[(String,AnyContent)] = async(BodyParsers.parse.default)(block)
final def async[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[(String,A)] =
composeAction(
new Action[(String, A)] {
def parser = DualBodyParser(parse.tolerantText, composeParser(bodyParser))
def apply(request: Request[(String,A)]) = try {
log.info(s"---- async - authorize: $authorizeOpt")
if (authorizeOpt.isEmpty || authorizeOpt.get.isAuthorized(request map (_._1))) {
invokeBlock(request map (_._2), block)
} else {
failUnauthorized("Not Authorized")
}
} catch {
case e: NotImplementedError => throw new RuntimeException(e)
case e: LinkageError => throw new RuntimeException(e)
}
override def executionContext = self.executionContext
}
)
def failUnauthorized(error: String): Future[Result] = Future.successful{
Ok(Json.stringify(Json.obj(
"result" -> "error",
"code" -> 401,
"message" -> error
)))
}
protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = bodyParser
protected def composeAction[A](action: Action[A]): Action[A] = action
override def andThen[Q[_]](other: ActionFunction[Request, Q]): ActionBuilder[Q] = new ActionBuilder[Q] {
def invokeBlock[A](request: Request[A], block: Q[A] => Future[Result]) =
self.invokeBlock[A](request, other.invokeBlock[A](_, block))
override protected def composeParser[A](bodyParser: BodyParser[A]): BodyParser[A] = self.composeParser(bodyParser)
override protected def composeAction[A](action: Action[A]): Action[A] = self.composeAction(action)
}
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]): Future[Result] = block(request)
}
}
Results
Let's finish our example, and we assume that we use Guice for providing concrete implementation of ourAuthorization
and TokenStore
traits:
import com.google.inject.{Inject, Provider}
import com.typesafe.config.Config
import net.codingwell.scalaguice.ScalaModule
case class TokenStoreImplementation(tokens: Seq[Token]) extends TokenStore {
val tokenMap = tokens.map(t => (t.publicKey, t)).toMap
def token(publicKey: String): Option[Token] = tokenMap.get(publicKey)
}
class TokenStoreProvider @Inject()(conf: Config) extends Provider[TokenStore] {
def get() = {
TokenStoreImplementation(
tokens = // get your tokens from config or db etc.
)
}
}
class AuthorizationProvider @Inject()(store: TokenStore) extends Provider[Authorization] {
def get() = new TokenAuthorization(
signatureCalculator = // provide your logic for calculating sign
store = store
)
}
object AuthorizationModule extends ScalaModule {
def configure(): Unit = {
bind[TokenStore].toProvider[TokenStoreProvider].asEagerSingleton()
bind[Authorization].toProvider[AuthorizationProvider].asEagerSingleton()
}
}
How lets take a look on how to define secured API with our authorization implemenetation with Blackjack and Hookers:
class HelloController @Inject() (authorize: Authorization) extends SecuredController(authorize) {
def helloWorld = SecuredAction { request =>
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
// …. some logic
Ok("Hello World")
}
def hello(name: String) = SecuredAction(parse.json) { request =>
// …. some logic
Ok(s"Hello $name")
}
}
Voilà, that is it! I hope you have enjoyed this article, and it will be helpful for you .
Cheers!
Resources:
Paper written by kvko
Ввійдіть щоб вподобати
Ввійдіть щоб прокоментувати