Alex Fruzenshtein
Alex Fruzenshtein

Reputation: 3126

How to combine AuthedRoutes and HttpRoutes in http4s?

Is there any rule how http4s prioritize AuthedRoutes over HttpRoutes when both of them are combined with <+> 👀

Because I receive 401 for such a combination authRoute <+> route, when calling:

GET /non-authed-route

The behavior changes, and I receive 200 for the same request, when I reorder the routes route <+> authRoute.

It may be even more messy when you need to work with 5+ routes which you need to combine and some of them may contain regular and secured endpoints simultaneously.

Upvotes: 3

Views: 2356

Answers (1)

Diego Alonso
Diego Alonso

Reputation: 761

So, though the question lacks some context information or code, I am going to assume that your problem is similar enough to a common one:

First, I assume your code has this approximate form, and is built with an AuthedRoutes.of (defined here), and an AuthMiddleware:

    val moonAuthedRoutes: AuthedRoutes[F, Int] = AuthedRoutes.of {
      case GET -> / sky / moon as T(42)
    }
    val marsRoutes: HttpRoutes[F] = HttpRoutes.of {
      case GET -> / sky / mars
    }
    // assuming you have your authedMiddleware 
    val moonRoutes = authedMiddleware(moonAuthedRoutes)
    val composedRoutes = moonRoutes <+> marsRoutes

Now, it helps if you understand the types of the aliases involved, and what they come down to once you unwrap Kleisli and OptionT:

type HttpRoutes[F, T] 
  = Kleisli[OptionT[F, ?], Request[F, T], Response[F]]
  ~~ Request[F] => F[Option[Response[F]]

type AuthedRoutes[F, T] 
  = Kleisli[OptionT[F, ?], AuthedRequest[F, T], Response[F]]
  ~~ (T, Request[F]) => F[Option[Response[F]]

In essence, a function that takes the HTTP request and some authentication context, and may give compute some response or none (inside a computation in F). To transform this AuthedRoutes into an HttpRoutes, you use an authentication middleware: a wrapper on your AuthedRoutes that takes the Request, tries to extract the T from it, if it succeeds it wraps the request with that T and passes it to the AuthedRoutes, as done in this line of code. If the middleware can not authenticate the request (that is extract the T), as of this line, it short-circuits and returns a 401 Unauthenticated response.

Now, the <+> composition first applies to the request on the moonRoutes. If the result of that is some response, that is what the composed routes returns. Only if the moonRoutes routes (at the left of <+>) return an F(None) will the composition pass the request on to marsRoutes. In particular, when you give it an unauthenticated request, the moonRoutes returns some 401 Unauthenticated response, and that is what the composedRoutes return, without ever passing the request on to marsRoutes.

If you swap the order, though, and write composedRoutes = marsRoutes <+? moonRoutes, then unauthenticated requests to the unauthenticated endpoints are answered in marsRoutes; whereas authenticated requests are first tried in marsRoutes, which fails (gives an F(None)), and then passed on to moonRoutes, which succeeds.

Upvotes: 9

Related Questions