Authentication
Built in
A service is a Kleisli[OptionT[F, ?], Request[F], Response[F]]
, the composable version of
Request[F] => OptionT[F, Response[F]]
. A service with authentication also requires some kind of User
object which identifies which user did the request. To reference the User
object
along with the Request[F]
, there’s AuthedRequest[F, User]
, which is equivalent to
(User, Request[F])
. So the service has the signature AuthedRequest[F, User] =>
OptionT[F, Response[F]]
, or AuthedService[User, F]
. So we’ll need a Request[F] => Option[User]
function, or more likely, a Request[F] => OptionT[F, User]
, because the User
can
come from a database. We can convert that into an AuthMiddleware
and apply it.
Or in code, using cats.effect.IO
as the effect type:
import cats._, cats.effect._, cats.implicits._, cats.data._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.server._
case class User(id: Long, name: String)
val authUser1: Kleisli[OptionT[IO, ?], Request[IO], User] = Kleisli(_ => OptionT.liftF(IO(???)))
// authUser1: Kleisli[OptionT[IO, β$0$], Request[IO], User] = Kleisli(
// <function1>
// )
val authMiddleware1: AuthMiddleware[IO, User] = AuthMiddleware(authUser1)
// authMiddleware1: Kleisli[OptionT[IO, β$1$], AuthedRequest[IO, User], Response[IO]] => Kleisli[OptionT[IO, β$1$], Request[IO], Response[IO]] = org.http4s.server.package$AuthMiddleware$$$Lambda$13572/275538414@558201e6
val authedService: AuthedService[User, IO] =
AuthedService {
case GET -> Root / "welcome" as user => Ok(s"Welcome, ${user.name}")
}
// authedService: AuthedService[User, IO] = Kleisli(
// org.http4s.AuthedService$$$Lambda$13573/1121920094@71c65190
// )
val service1: HttpService[IO] = authMiddleware1(authedService)
// service1: HttpService[IO] = Kleisli(
// org.http4s.server.package$AuthMiddleware$$$Lambda$13574/728804549@55b0001e
// )
Returning an Error Response
Usually, it should also be possible to send back a 401 in case there was no
valid login. The 401 response can be adjusted as needed, some applications use a
redirect to a login page, or a popup requesting login data. With the upcoming of
SPA, the correct http status codes are relevant again.
With Kleisli
To allow for failure, the authUser
function has to be adjusted to a Request[F]
=> F[Either[String,User]]
. So we’ll need to handle that possibility. For advanced
error handling, we recommend an error ADT instead of a String
.
val authUser2: Kleisli[IO, Request[IO], Either[String,User]] = Kleisli(_ => IO(???))
// authUser2: Kleisli[IO, Request[IO], Either[String, User]] = Kleisli(
// <function1>
// )
val onFailure2: AuthedService[String, IO] = Kleisli(req => OptionT.liftF(Forbidden(req.authInfo)))
// onFailure2: AuthedService[String, IO] = Kleisli(<function1>)
val authMiddleware2 = AuthMiddleware(authUser2, onFailure2)
// authMiddleware2: Kleisli[OptionT[IO, β$1$], AuthedRequest[IO, User], Response[IO]] => Kleisli[OptionT[IO, β$1$], Request[IO], Response[IO]] = org.http4s.server.package$AuthMiddleware$$$Lambda$13575/1857086211@71d91ccd
val service2: HttpService[IO] = authMiddleware2(authedService)
// service2: HttpService[IO] = Kleisli(
// cats.data.Kleisli$$$Lambda$13582/379054697@3491be21
// )
Implementing authUser
There’s a few different ways to send authorization information with a HTTP
request, the two most common are cookie for regular browser usage or the
Authorization
header for SPA.
Cookies
We’ll use a small library for the signing/validation of the cookies, which
basically contains the code used by the Play framework for this specific task.
libraryDependencies += "org.reactormonk" %% "cryptobits" % "1.2"
First, we’ll need to set the cookie. For the crypto instance, we’ll need to
provide a private key. You usually want to set a static secret so people don’t
lose their session on server restarts, and a static secret also allows you to
use multiple application instances.
The message is simply the user id.
import org.reactormonk.{CryptoBits, PrivateKey}
import java.time._
val key = PrivateKey(scala.io.Codec.toUTF8(scala.util.Random.alphanumeric.take(20).mkString("")))
// key: PrivateKey = PrivateKey(
// Array(
// 117,
// 113,
// 120,
// 80,
// 74,
// 106,
// 118,
// 65,
// 97,
// 57,
// 54,
// 108,
// 89,
// 79,
// 116,
// 71,
// 98,
// 70,
// 74,
// 97
// )
// )
val crypto = CryptoBits(key)
// crypto: CryptoBits = CryptoBits(
// PrivateKey(
// Array(
// 117,
// 113,
// 120,
// 80,
// 74,
// 106,
// 118,
// 65,
// 97,
// 57,
// 54,
// 108,
// 89,
// 79,
// 116,
// 71,
// 98,
// 70,
// 74,
// 97
// )
// )
// )
val clock = Clock.systemUTC
// clock: Clock = SystemClock[Z]
def verifyLogin(request: Request[IO]): IO[Either[String,User]] = ??? // gotta figure out how to do the form // gotta figure out how to do the form
val logIn: Kleisli[IO, Request[IO], Response[IO]] = Kleisli({ request =>
verifyLogin(request: Request[IO]).flatMap(_ match {
case Left(error) =>
Forbidden(error)
case Right(user) => {
val message = crypto.signToken(user.id.toString, clock.millis.toString)
Ok("Logged in!").map(_.addCookie(Cookie("authcookie", message)))
}
})
})
// logIn: Kleisli[IO, Request[IO], Response[IO]] = Kleisli(<function1>)
Now that the cookie is set, we can retrieve it again in the authUser
.
def retrieveUser: Kleisli[IO, Long, User] = Kleisli(id => IO(???))
val authUser3: Kleisli[IO, Request[IO], Either[String,User]] = Kleisli({ request =>
val message = for {
header <- headers.Cookie.from(request.headers).toRight("Cookie parsing error")
cookie <- header.values.toList.find(_.name == "authcookie").toRight("Couldn't find the authcookie")
token <- crypto.validateSignedToken(cookie.content).toRight("Cookie invalid")
message <- Either.catchOnly[NumberFormatException](token.toLong).leftMap(_.toString)
} yield message
message.traverse(retrieveUser.run)
})
// authUser3: Kleisli[IO, Request[IO], Either[String, User]] = Kleisli(
// <function1>
// )
There is no inherent way to set the Authorization header, send the token in any
way that your SPA understands. Retrieve the header value in the authUser
function.
import org.http4s.util.string._
import org.http4s.headers.Authorization
val authUser4: Kleisli[IO, Request[IO], Either[String,User]] = Kleisli({ request =>
val message = for {
header <- request.headers.get(Authorization).toRight("Couldn't find an Authorization header")
token <- crypto.validateSignedToken(header.value).toRight("Cookie invalid")
message <- Either.catchOnly[NumberFormatException](token.toLong).leftMap(_.toString)
} yield message
message.traverse(retrieveUser.run)
})
// authUser4: Kleisli[IO, Request[IO], Either[String, User]] = Kleisli(
// <function1>
// )
Using tsec-http4s for Authentication and Authorization
The TSec project provides an authentication and authorization module
for the http4s project 0.18-M4+. Docs specific to http4s are located Here.