fokot.github.io

Authentication in Caliban

2019-12-16 on Caliban, ZIO, GraphQL, Scala

We are building GraphQL service at work and we decided to use Caliban - a new promising GraphQL backend in Scala. Library is based on ZIO which makes many things simpler. In this article I will show you how to do the authentication in Caliban.

ZIO

Let's look at ZIO first. ZIO[R, E, A] has three parameters, the fist one is environment aka dependencies. second one in error type and the last one is carried value. In this example we will use simple type alias where the error is Throwable type RIO[-R, +A] = ZIO[R, Throwable, A] To access the environment we use access method ZIO.access[Console](_.console.putStrLn("I'm here")) It is also possible to use accessM if the value returned will be another ZIO or environment which will fill the A with R


import zio.clock.Clock
import zio.console.Console

object ZioTest extends zio.App {

  //  type ZEnv = Clock with Console with System with Random with Blocking

  val welcome = ZIO.accessM[Console](_.get.putStrLn("Welcome"))

  val getHours = ZIO.accessM[Clock](_.get.currentDateTime)

  override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
    for {
      _ <- welcome
      h <- getHours
      _ <- ZIO.accessM[Console](_.get.putStrLn(h.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)))
    } yield 0
}
If we want to provide R to run our ZIO we can use provide method to provide new constant R or we can use provideSome eliminate some of the dependencies (construct new R from existing)

trait MyService {
  def getValue: String
}

override def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
  (ZIO.accessM[MyService with Console](
    r =>
    r.console.putStrLn(r.getValue)
  ) as 0)
    .provideSome((env: ZEnv) => new MyService with Console {
      override def getValue: String = "I'm cool service"
      override val console: Console.Service[Any] = env.console
    })
     

Caliban

Now we know enough to use it with Caliban. In Caliban GraphQL schema is derived from case classes. Every field can be either the case class of result type or ZIO of that type.


// environment used to resolve the schema
type Env = Storage with Console with Auth with WC[Config] with Clock

// read value lazily
type Z[A] = RIO[Env, A]

case class Queries(
  allBooks: Z[List[Book]],
  myBooks: Z[List[Book]]
)
  
Imagine I want to access allBooks only by Admin and myBooks by all logged in users. With ZIO it can be done easily

sealed trait Role
object Role {
  final case object Editor extends Role
  final case object Viewer extends Role
}

case class User(
  login: String,
  role: Role
)

type Auth = Has[AuthService[Any]]

trait AuthService[R] {
  def token: RIO[R, User]
}

type Authorized = RIO[Auth, User]

val isAuthenticated: Authorized = ZIO.accessM[Auth](_.auth.token)

/**
 * Will succeed if user has at least one of specified roles
 */
def hasRole(r: Role*): Authorized = isAuthenticated.filterOrFail(t => r.contains(t.role))(AuthException("Permission denied"))

val isAdmin: Authorized = hasRole(Role.Admin)

val allBooks: Z[List[Book]] =
  isAdmin *> ZIO.accessM[Env](_.storage.getAllBooks().map(_.map(bookToGQL)))

val myBooks: Z[List[Book]] =
  isAuthenticated >>= (u => ZIO.accessM[Env](_.storage.getBooksForUser(u).map(_.map(bookToGQL))))
  
a *> b is like flatMap ignoring value produced by first effect, so it can be rewritten to a.flatMap(_ => b). If we need value like in myBooks we can use flatMap or its alias >>=.

Resolver's part is done, now we need to get Auth service to our environment. I created simple AuthService implementation which can be initialised with Task[User]


trait AuthService[R] {
  def currentUser: RIO[R, User]
}

case class SimpleService(currentUser: Task[User]) extends AuthService[Any]
  
And then I can parse the token in http4s and pass it to the environment with provideSome. You can find the code in the Main class. The idea is to extract User from token in ZEnv and the provide initialised Auth service to GraphQL interpreter

HttpRoutes
  .of[RIO[ZEnv, *]] {
    case req@POST -> Root / "api" / "graphql" => {
      // this runs with environment ZEnv
      val user = extractUserFromToken(request.headers.get(Authorization).map(_.value))
      val query = req.attemptAs[GraphQLRequest].value.absolve
      // this runs with environment Auth
      interpreter
        .execute(query.query, query.operationName, query.variables.getOrElse(Map()))
        .provide(SimpleService(user))
    }
  

Final words

Full example can be found on my github. Definitely take a look at Caliban and ZIO, it's awesome.