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.

Alexey Novakov

September 28, 2020
Tweet

More Decks by Alexey Novakov

Other Decks in Programming

Transcript

  1. RHEIN-MAIN SCALA MEETUP, ALEXEY NOVAKOV, ULTRA TENDENCY
    Functional Web Programming in Scala
    HTTP4S MIDDLEWARE

    View Slide

  2. ➤ Solution Architect at Ultra
    Tendency
    ➤ Experience:
    ➤ Scala - 5yrs.
    ➤ Java - 10 yrs.
    ➤ Big Data, Kubernetes
    ➤ My interests:
    ➤ 8 string guitars
    ➤ Astronomy
    ➤ FP, Scala, Rust

    View Slide

  3. -http4s overview *
    -Kleisli
    -Middleware
    -SPNEGO example
    CONTENT
    * Knowledge of Cats-Effect are desired to have

    View Slide

  4. HTTP4S: TYPEFUL, FUNCTIONAL, STREAMING

    View Slide

  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)

    View Slide

  6. TYPEFUL

    View Slide

  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

    View Slide

  8. FUNCTIONAL

    View Slide

  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!!!

    View Slide

  10. STREAMING

    View Slide

  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)

    View Slide

  12. KLEISLI : A => F[B]
    KLEISLI ARROW COMES FROM CATEGORY THEORY AND IS NAMED
    AFTER THE SWISS MATHEMATICIAN HEINRICH KLEISLI

    View Slide

  13. - compose functions which return monadic values
    PURPOSE
    f1: String => F[Int]
    f2: Int => F[Double]
    String => F[Double]
    f1 andThen f2

    View Slide

  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]) {

    View Slide

  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))

    View Slide

  16. HTTP4S MIDDLEWARE

    View Slide

  17. MIDDLEWARE IS A WRAPPER
    AROUND YOUR SERVICE

    View Slide

  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.)

    View Slide

  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]]]

    View Slide

  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

    View Slide

  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

    View Slide

  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"))

    View Slide

  23. SPNEGO MIDDLEWARE

    View Slide

  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 = :88

    View Slide

  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
    Status 401
    Header: Authorization
    Negotiate
    6. GET /auth
    7. Header:
    Cookie
    Status 200
    SPNEGO
    Kerberos KDC
    browser sends this request automatically

    View Slide

  26. CURL SUPPORT
    curl --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt \
    http://myserver/auth
    --negotiate Use HTTP Negotiate (SPNEGO) authentication

    View Slide

  27. IMPLEMENTATION

    View Slide

  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)
    }

    View Slide

  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)

    View Slide

  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, …)

    View Slide

  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]]

    View Slide

  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]]
    }

    View Slide

  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)
    }

    View Slide

  34. JAVA API
    IMPLEMENTATION

    View Slide

  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 :-)

    View Slide

  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

    View Slide

  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()

    View Slide

  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)
    }
    }
    )

    View Slide

  39. THAT IS IT

    View Slide

  40. - Available on GitHub
    OPEN-SOURCE

    View Slide

  41. SUMMARY
    -http4s can be extended with your new middleware easily
    -standard JRE library is all you need for SPNEGO

    View Slide

  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

    View Slide