Last active
December 26, 2021 01:17
-
-
Save blast-hardcheese/ce10c703381e2d7397c3e6091397628b to your computer and use it in GitHub Desktop.
guardrail http4s basic authentication example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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