Upgrade to Pro — share decks privately, control downloads, hide ads and more …

http4s middleware

http4s middleware

Using so called "middleware", we can extend the functionality of the
http layer provided by http4s. The actual work required to implement
your own middleware are surprisingly simple and more fun than you
might think at first. Once you dive into implementing your own
middleware, you can easily implement things like your own
authentication mechanism, metrics reporter or data enrichment layer.

7a04b88e1469561db6da3818348d4b8f?s=128

Alexey Novakov

September 28, 2020
Tweet

Transcript

  1. RHEIN-MAIN SCALA MEETUP, ALEXEY NOVAKOV, ULTRA TENDENCY Functional Web Programming

    in Scala HTTP4S MIDDLEWARE
  2. ➤ Solution Architect at Ultra Tendency ➤ Experience: ➤ Scala

    - 5yrs. ➤ Java - 10 yrs. ➤ Big Data, Kubernetes ➤ My interests: ➤ 8 string guitars ➤ Astronomy ➤ FP, Scala, Rust
  3. -http4s overview * -Kleisli -Middleware -SPNEGO example CONTENT * Knowledge

    of Cats-Effect are desired to have
  4. HTTP4S: TYPEFUL, FUNCTIONAL, STREAMING

  5. ENVIRONMENT import cats.effect._, org.http4s._, org.http4s.dsl.io._, import scala.concurrent.ExecutionContext.Implicits.global implicit val cs:

    ContextShift[IO] = IO.contextShift(global) implicit val timer: Timer[IO] = IO.timer(global)
  6. TYPEFUL

  7. val helloWorldService: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root

    / "hello" / name => Ok(s"Hello, $name.") } type HttpRoutes[F[_]] = Http[OptionT[F, *], F] type Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]] final case class Kleisli[F[_], -A, B](run: A => F[B]) Cats Core
  8. FUNCTIONAL

  9. val helloWorldService = HttpRoutes.of[IO] { case GET -> Root /

    "hello" / name => Ok(s"Hello, $name.") } val response = io.unsafeRunSync() // response: Response[IO] = Response( // Status(200), // HttpVersion(1, 1), // Headers(), // Stream(..), … // ) val req = Request[IO](Method.GET, uri"/hello/tom") val io: IO[Response[IO]] = helloWorldService.orNotFound.run(req) TESTING WITHOUT STARTING THE WEB-SERVER!!!
  10. STREAMING

  11. streaming is powered by fs2 import fs2.Stream Ok(drip) // res14:

    IO[Response[IO]] = Pure( // Response( // Status(200), // HttpVersion(1, 1), // Headers(Content-Type: text/plain; charset=UTF-8, Transfer-Encoding: chunked), // Stream(..), // ) // ) val drip: Stream[IO, String] = Stream.awakeEvery[IO](100.millis).map(_.toString).take(10)
  12. KLEISLI : A => F[B] KLEISLI ARROW COMES FROM CATEGORY

    THEORY AND IS NAMED AFTER THE SWISS MATHEMATICIAN HEINRICH KLEISLI
  13. - compose functions which return monadic values PURPOSE f1: String

    => F[Int] f2: Int => F[Double] String => F[Double] f1 andThen f2
  14. CATS IMPLEMENTATION import cats.FlatMap import cats.implicits._ def compose[Z](k: Kleisli[F, Z,

    A])(implicit F: FlatMap[F]) : Kleisli[F, Z, B] = Kleisli[F, Z, B](z => k.run(z).flatMap(run)) } final case class Kleisli[F[_], A, B](run: A => F[B]) {
  15. EXAMPLE def findCluster(ns: String): Option[Int] = if (ns == "app-1")

    Some(3) else None def scaleCluster(replicas: Int): Option[String] = if (replicas > 0) Some("done") else None > scaleExistingCluster.run("app-1") res23: Option[String] = Some("done") val scaleExistingCluster = Kleisli(scaleCluster).compose(Kleisli(findCluster))
  16. HTTP4S MIDDLEWARE

  17. MIDDLEWARE IS A WRAPPER AROUND YOUR SERVICE

  18. def myMiddleware(routes: HttpRoutes[F]): HttpRoutes[F] = ??? - http4s OTB middleware:

    - Authentication - CORS - GZip - Metrics - and others - Wrapper can: - enrich request, response - cancel request by returning 404 or 500 status - run additional side-effect (collect metrics, etc.)
  19. MIDDLEWARE TYPE Kleisli[OptionT[F, *], Request[F], Response[F]] type HttpRoutes[F[_]] = Http[OptionT[F,

    *], F] type Http[F[_], G[_]] = Kleisli[F, Request[G], Response[G]] final case class OptionT[F[_], A](value: F[Option[A]]) Kleisli(run: Request[F] => OptionT[F, Response[F]]]
  20. MIDDLEWARE CONSTRUCTION Kleisli { (req: Request[IO]) => // do something

    with the request val response = originalService(req) // do something with the response response } val originalService: HttpRoutes[IO] = ??? logic
  21. def myMiddleware(service: HttpRoutes[IO], header: Header) : HttpRoutes[IO] = Kleisli {

    (req: Request[IO]) => service(req).map { case Status.Successful(resp) => resp.putHeaders(header) case resp => resp } } PUT HEADER MIDDLEWARE
  22. START SERVER BlazeServerBuilder[F] .bindHttp(8080, "0.0.0.0") .withHttpApp(routes.orNotFound) .serve.compile.drain.as(ExitCode.Success) val routes =

    myMiddleware( helloWorldService, Header("someHeader", "itsValue"))
  23. SPNEGO MIDDLEWARE

  24. - SPNEGO - Simple and Protected GSSAPI Negotiation Mechanism (RFC

    4178, RFC 2743) - Generic Security Services API - client and server negotiate the choice of security technology and performs security operations (authentication, etc.) - extensively used by MS Internet Explorer and Active Directory based application - supported by Chrome, Firefox EXAMPLE.COM /etc/krb5.conf http://example.com = EXAMPLE.COM EXAMPLE.COM = <kdc_hostname>:88
  25. SERVER APP Client App OS Internet Browser 2. kerberos ticket

    1. login SPNEGO trusted uris 3. http://some-trusted-uri/auth 4. GET /auth 5. Header: WWW-Authenticate: Negotiate <server_token> Status 401 Header: Authorization Negotiate <client_token> 6. GET /auth 7. Header: Cookie <encrypted session token> Status 200 SPNEGO Kerberos KDC browser sends this request automatically
  26. CURL SUPPORT curl --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt

    \ http://myserver/auth --negotiate Use HTTP Negotiate (SPNEGO) authentication
  27. IMPLEMENTATION

  28. SPNEGO import org.http4s.server.AuthMiddleware import org.http4s._ class Spnego[F[_]: Sync](cfg: SpnegoConfig) {

    val authenticator = new SpnegoAuthenticator[F](cfg, …) val authToken: Kleisli[F, Request[F], Either[Rejection, AuthToken]] = Kleisli(request => authenticator.apply(request.headers)) val onFailure: AuthedRoutes[Rejection, F] = ??? val middleware: AuthMiddleware[F, AuthToken] = AuthMiddleware(authToken, onFailure) def apply(service: AuthedRoutes[AuthToken, F]): HttpRoutes[F] = middleware(service) }
  29. USAGE import org.http4s.dsl.Http4sDsl import org.http4s.{AuthedRoutes, HttpRoutes} val authRoutes = AuthedRoutes.of[AuthToken,

    F] { case GET -> Root as token => Ok(s"This page is protected using HTTP SPNEGO authentication;" + s" logged in as $token") .map(_.addCookie(spnego.signCookie(token))) } val spnego = new Spnego[F](cfg) val routes: HttpRoutes[F] = spnego(authRoutes)
  30. TYPES: ROUTES type AuthedRoutes[T, F[_]] = Kleisli[OptionT[F, *], AuthedRequest[F, T],

    Response[F]] type AuthedRequest[F[_], T] = ContextRequest[F, T] final case class ContextRequest[F[_], A](context: A, req: Request[F]) A - is AuthToken type, in case of Spengo middleware case class AuthToken(principal: String, expiration: Long, …)
  31. TYPES: MIDDLEWARE type AuthMiddleware[F[_], T] = Middleware[OptionT[F, *], AuthedRequest[F, T],

    Response[F], Request[F], Response[F]] type Middleware[F[_], A, B, C, D] = Kleisli[F, A, B] => Kleisli[F, C, D] Kleisli[OptionT[F, *], AuthedRequest[F, T], Response[F]] => Kleisli[OptionT[F, *], Request[F], Response[F]]
  32. AUTHENTICATOR class SpnegoAuthenticator[F[_]: Sync](…) { def apply(hs: Headers): F[Either[Rejection, AuthToken]]

    = cookieToken(hs) .orElse(kerberosNegotiate(hs)) .getOrElseF(initiateNegotiations) // cookie found, so nothing to do, return it back def cookieToken(hs: Headers): OptionT[F, Either[Rejection, AuthToken]] // No cookie, client token found, trying to authenticate def kerberosNegotiate(hs: Headers): OptionT[F, Either[Rejection, AuthToken]] // No cookie, no client token found, so sending WWW-Authenticate back def initiateNegotiations: F[Either[Rejection, AuthToken]] }
  33. ON FAILURE val onFailure: AuthedRoutes[Rejection, F] = Kleisli { req

    => val rejection = req.context match { case AuthenticationFailedRejection(r, h) => (reasonToString(r), Seq(h)).pure[F] case MalformedHeaderRejection(name, msg, cause) => … case ServerErrorRejection(e) => … … } OptionT.liftF(for { (msg, headers) <- rejection res = Response[F](Status.Unauthorized) .putHeaders(headers: _*) .withEntity(msg) } yield res) }
  34. JAVA API IMPLEMENTATION

  35. JAVA - JRE 1.4 (2002 release) - Classes for Kerberos

    import org.ietf.jgss.{GSSCredential, GSSManager} import javax.security.auth.Subject import javax.security.auth.kerberos.KerberosPrincipal import javax.security.auth.login.LoginContext import java.security.{PrivilegedAction, PrivilegedActionException, PrivilegedExceptionAction} - Pitfalls: every method may throw an exception … or return null … or takes null as a valid parameter :-)
  36. WIN - no external Scala/Java library is needed - just

    wrap Java API with nice Scala facade "org.springframework.security.kerberos" % "spring-security-kerberos-web" % "1.0.1.RELEASE" Examples using Java API: Server & Client: https://docs.oracle.com/javase/10/security/... Client: https://medium.com/se-notes-by-alexey-novakov/spnego-token-generation-90c4267f784e
  37. SERVER SIDE: STARTUP val subject = new Subject( false, Collections.singleton(new

    KerberosPrincipal(cfg.principal)), Collections.emptySet(), Collections.emptySet() ) val noCallback = null val lc = new LoginContext(entryName, subject, noCallback, kerberosConfiguration.orNull) val gssManager = Subject.doAs( lc.getSubject, new PrivilegedAction[GSSManager] { override def run: GSSManager = GSSManager.getInstance } ) lc.login()
  38. SERVER SIDE: AUTHENTICATE Subject.doAs( lc.getSubject, new PrivilegedExceptionAction[(Option[Array[Byte]], Option[AuthToken])] { override

    def run: (Option[Array[Byte]], Option[AuthToken]) = { val defaultAcceptor: GSSCredential = null val gssContext = gssManager.createContext(defaultAcceptor) val serverToken = Option(gssContext.acceptSecContext(clientToken, 0, clientToken.length)) val authToken = if (gssContext.isEstablished) Some(tokens.create(gssContext.getSrcName.toString)) else None gssContext.dispose() (serverToken, authToken) } } )
  39. THAT IS IT

  40. - Available on GitHub OPEN-SOURCE

  41. SUMMARY -http4s can be extended with your new middleware easily

    -standard JRE library is all you need for SPNEGO
  42. THANKS! QUESTIONS? Alexey Novakov email: - alexey.novakov a_t ultratendency.com -

    novakov.alex a_t gmail.com Blog: https://novakov-alexey.github.io/ https://medium.com/se-notes-by-alexey-novakov Code: https://github.com/novakov-alexey/http4s-spnego Twitter: @alexey_novakov