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. ➤ Solution Architect at Ultra Tendency ➤ Experience: ➤ Scala

    - 5yrs. ➤ Java - 10 yrs. ➤ Big Data, Kubernetes ➤ My interests: ➤ 8 string guitars ➤ Astronomy ➤ FP, Scala, Rust
  2. 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
  3. 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!!!
  4. 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)
  5. KLEISLI : A => F[B] KLEISLI ARROW COMES FROM CATEGORY

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

    => F[Int] f2: Int => F[Double] String => F[Double] f1 andThen f2
  7. 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]) {
  8. 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))
  9. 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.)
  10. 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]]]
  11. 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
  12. 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
  13. - 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
  14. 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
  15. CURL SUPPORT curl --negotiate -u : -b ~/cookiejar.txt -c ~/cookiejar.txt

    \ http://myserver/auth --negotiate Use HTTP Negotiate (SPNEGO) authentication
  16. 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) }
  17. 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)
  18. 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, …)
  19. 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]]
  20. 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]] }
  21. 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) }
  22. 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 :-)
  23. 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
  24. 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()
  25. 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) } } )
  26. SUMMARY -http4s can be extended with your new middleware easily

    -standard JRE library is all you need for SPNEGO
  27. 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