Authorization with Autowire
I've been playing with Scala.js lately. I came across Autowire, an excellent RPC library for typesafe server-client communication. However, I couldn't find any examples of how to bake authorization into my RPC calls. The following example is a simple solution that is agnostic to your application stack and doesn't pollute your pretty method signatures.
TL;DR. Inject the User
/authorization object into your API implementation
and create a new server instance for each RPC call.
This post assumes you understand how Autowire works. The "trick" may appear obvious in hindsight, but it took me a while to figure out.
So let's begin, we want to restrict access to an AdminApi
// Shared project.
trait AdminApi {
def doSecretThing(secret: String): String
}
case class HttpError(status: Int) extends Exception
object UnauthorizedError extends HttpError(401)
case class User(roles: List[String])
In this example we will throw an exception in case authorization fails. If you
don't like exceptions, I'm sure you can figure out how to use Either
instead.
On the server side, we implement the interface
// Server project.
class AdminApiImpl(user: User) extends AdminApi {
// Authorization run in constructor for all methods.
if (!user.roles.contains("admin")) throw UnauthorizedError
def doSecretThing(secret: String): String =
// Authorization run only for this method.
if (user.roles.contains("superadmin")) secret
else throw UnauthorizedError
}
Most importantly, our AdminApi
implementation has a user member. That is, we
create a new instance of our API for every RCP request. With this setup, we can
run authorization logic either in methods or the constructor.
We implement the server in a similar fashion.
// Server project.
class MyServer(user: User) extends autowire.Server[...] {
def write[...](r: AutowireResult) = ...
def read[...](p: String) = ...
// This is the interesting bit.
val adminApi = route[AdminApi](new AdminApiImpl(user))
val routes = adminApi
}
The triple dots ...
hide some parts that are irrelevant for the purpose of
this example. The key bit is that the server also has a user member, like
AdminApiImpl
.
We can compose multiple APIs together like this.
// Server project, inside MyServer
val adminApi = route[AdminApi](new AdminApiImpl(user))
val basicApi = route[BasicApi](BasicApiImpl)
val routes = adminApi orElse basicApi
In our request handler, we create a new instance of MyServer
for every rpc
call. The following piece is implemented in the Play framework, but you
can adapt it to your needs. For example, if you are using Spray as a
server or Silhouette for authentication, your request handler will
look differently.
// Server project.
object Application extends Controller {
def autowireApi = Action.async { request =>
try {
// Made up method name, this will depend on
// your stack.
val user: User = getUserFromRequest(request)
// Use your favorite serialization format.
val autowireRequest = ...
// The important bit, pass user into server.
new MyServer(user).routes(autowireRequest)
.map { txt =>
Ok(txt)
}
} catch {
// Standard http response in case of failure.
case HttpError(s) =>
Future.successful(new Status(s))
}
}
}
From the client, invoke the RPC like usual
// Client project.
MyClient[AdminApi].doSecretThing("I am an admin.")
.call().map { msg =>
println(msg) // "I am an admin."
}.recover {
case AjaxException(xhr) if xhr.status == 401 => {
println("I am not an admin.")
}
}
There we go, no frills authorization with Autowire. You should be able adapt this idea to your favorite application stack. If you can think of a better way to accomplish the same thing, I would be happy to hear about it.
For more information on writing a SPA with Scala.js, I recommend this tutorial.