Skip to content

Instantly share code, notes, and snippets.

@blast-hardcheese
Last active December 26, 2021 01:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save blast-hardcheese/ce10c703381e2d7397c3e6091397628b to your computer and use it in GitHub Desktop.
Save blast-hardcheese/ce10c703381e2d7397c3e6091397628b to your computer and use it in GitHub Desktop.
guardrail http4s basic authentication example
diff --git a/modules/sample-http4s/src/test/scala/core/Http4s/Http4sRoundTripTest.scala b/modules/sample-http4s/src/test/scala/core/Http4s/Http4sRoundTripTest.scala
index b0641e74..d8cf827f 100644
--- a/modules/sample-http4s/src/test/scala/core/Http4s/Http4sRoundTripTest.scala
+++ b/modules/sample-http4s/src/test/scala/core/Http4s/Http4sRoundTripTest.scala
@@ -3,6 +3,7 @@ package core.Http4s
import java.security.MessageDigest
import java.util.Locale.US
+import cats.data.Kleisli
import _root_.examples.client.http4s.pet.PetClient
import _root_.examples.client.{ http4s => cdefs }
import _root_.examples.server.http4s.pet._
@@ -13,14 +14,19 @@ import cats.effect.IO._
import cats.effect.unsafe.implicits.global
import fs2.Stream
import javax.xml.bind.DatatypeConverter.printHexBinary
+import org.http4s.{Request, Response}
import org.http4s.client.Client
import org.http4s.implicits._
+import org.http4s.headers._
import org.scalatest.exceptions.TestFailedException
import org.scalatest.EitherValues
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import examples.support.PositiveLong
+import cats.Applicative
+import org.http4s.{AuthScheme, BasicCredentials, Credentials, Header, HttpApp, Status}
+import cats.arrow.FunctionK
class Http4sRoundTripTest extends AnyFunSuite with Matchers with EitherValues {
@@ -38,21 +44,43 @@ class Http4sRoundTripTest extends AnyFunSuite with Matchers with EitherValues {
val tag3name: Option[String] = None
val petStatus: Option[String] = Some("pending")
- test("round-trip: definition query, unit response") {
- val httpService = new PetResource().routes(new PetHandler[IO] {
- def addPet(respond: AddPetResponse.type)(body: sdefs.definitions.Pet): IO[sdefs.pet.PetResource.AddPetResponse] =
- body match {
- case sdefs.definitions.Pet(
- `id`,
- Some(sdefs.definitions.Category(`categoryId`, `categoryName`)),
- `name`,
- `photoUrls`,
- None,
- Some(sdefs.definitions.PetStatus.Pending)
- ) =>
- IO.pure(respond.Created)
- case _ => throw new TestFailedException("Parameters didn't match", 11)
+ test("round-trip: definition query, unit response (!)") {
+ // The crux of this authentication example is to replace IO as our F with a Kleisli that is capable of deriving state from the request.
+ //
+ // As the F[Response[F]] has yet to be evaluated, it then falls to us to actually do that evaluation,
+ // lifting the result of that back into the kleisli.
+ //
+ // This is not the most elegant solution, but it proves that this is possible today (and quite flexible, as it turns out),
+ // so it gives us some time to implement authentication with clear attention to the specification.
+ type F[A] = Kleisli[IO, Option[String], A]
+
+ val authMiddleware: (String, Request[F], F[Response[F]]) => F[Response[F]] = { case (routeName, req, resp) =>
+ req.headers.get[Authorization]
+ .flatMap {
+ case Authorization(BasicCredentials(("foo", "bar"))) => Some(Kleisli[IO, Option[String], Response[F]](_ => resp(Some("user foo was authenticated"))))
+ case _ => None
}
+ .getOrElse(Applicative[F].pure(Response[F](Status.BadRequest)))
+ }
+
+ val httpService = new PetResource[F](mapRoute = authMiddleware).routes(new PetHandler[F] {
+ def addPet(respond: AddPetResponse.type)(body: sdefs.definitions.Pet): F[sdefs.pet.PetResource.AddPetResponse] =
+ Kleisli[IO, Option[String], sdefs.pet.PetResource.AddPetResponse]({
+ case Some("user foo was authenticated") =>
+ body match {
+ case sdefs.definitions.Pet(
+ `id`,
+ Some(sdefs.definitions.Category(`categoryId`, `categoryName`)),
+ `name`,
+ `photoUrls`,
+ None,
+ Some(sdefs.definitions.PetStatus.Pending)
+ ) =>
+ Applicative[IO].pure(respond.Created)
+ case _ => throw new TestFailedException("Parameters didn't match", 11)
+ }
+ case _ => throw new TestFailedException("Auth did not match", 12)
+ })
def deletePet(
respond: DeletePetResponse.type
)(_petId: Long, includeChildren: Option[Boolean], status: Option[sdefs.definitions.PetStatus], apiKey: Option[String]) = ???
@@ -65,17 +93,36 @@ class Http4sRoundTripTest extends AnyFunSuite with Matchers with EitherValues {
def uploadFile(respond: UploadFileResponse.type)(
petId: PositiveLong,
additionalMetadata: Option[String] = None,
- file: Option[fs2.Stream[IO, Byte]] = None,
- file2: fs2.Stream[IO, Byte],
- file3: fs2.Stream[IO, Byte],
+ file: Option[fs2.Stream[F, Byte]] = None,
+ file2: fs2.Stream[F, Byte],
+ file3: fs2.Stream[F, Byte],
longValue: Long,
customValue: PositiveLong,
customOptionalValue: Option[PositiveLong] = None
) = ???
})
- val petClient = PetClient.httpClient(Client.fromHttpApp(httpService.orNotFound))
+ import org.http4s.blaze.server._
+
+ val service: Kleisli[F, Request[F], Response[F]] = httpService.orNotFound
+ // In order to translate our F[_] down to IO...
+ val httpApp: HttpApp[IO] = service.translate[IO](new FunctionK[F, IO] {
+ // we can just supply a default of `None` to go from Kleisli[IO, Option[String], A] to IO[A]
+ def apply[A](fa: F[A]): IO[A] = fa.run(None)
+ })(new FunctionK[IO, F] {
+ // and conversely just ignore the input to lift the IO[A] into Kleisli[IO, Option[String], A]
+ def apply[A](fa: IO[A]): F[A] = Kleisli(_ => fa)
+ })
+ // Now that we have an HttpApp[IO], we can use that in Client.fromHttpApp(httpApp),
+ // but since we need to inject headers we can do this client-on-top-of-client thing.
+ //
+ // Presumably there's a better way to do it, but this was just a PoC, and this will not be used
+ // in production code, as supplying authentication headers is the job of the client.
+ val petClient = PetClient.httpClient(Client[IO](req => Client.fromHttpApp(httpApp).run(req.withHeaders(Authorization(BasicCredentials("foo", "bar"))))))
+ // Client code is unchanged. By adding an `Authorization` header to the OpenAPI specification,
+ // it may be possible to supply the header values as parameters to the client invocation,
+ // but that is left as an exercise for the reader.
petClient
.addPet(
cdefs.definitions.Pet(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment