-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reverse routing with router #8
Comments
Hey @shagoon, I would suggest building the import cats.effect.IO
import cats.syntax.semigroupk._
import org.http4s.{HttpRoutes, Request}
import org.http4s.dsl.io._
import org.http4s.server.middleware.GZip
import routing._
import routing.http4s._
// Apply the "protected" prefix here
def protectedRoute[M <: Method, P](method: M)(f: Route[M, Unit] => Route[M, P]): Route[M, P] =
f(method / "protected")
val protectedGetRoute = protectedRoute(Method.GET)(_ / "get-route")
val protectedPostRoute = protectedRoute(Method.POST)(_ / "post-route")
// Apply your tsec middleware here, note I'm using gzip just as an example
val protectedRoutes: HttpRoutes[IO] = GZip(Route.httpRoutes[IO](
protectedGetRoute.handle(_ => (_: Request[IO]) => Ok("protected GET")),
protectedPostRoute.handle(_ => (_: Request[IO]) => Ok("protected POST"))
))
val publicGetRoute = Method.GET / "get-route"
val publicPostRoute = Method.POST / "post-route"
val publicRoutes: HttpRoutes[IO] = Route.httpRoutes[IO](
publicGetRoute.handle(_ => (_: Request[IO]) => Ok("public GET")),
publicPostRoute.handle(_ => (_: Request[IO]) => Ok("public POST"))
)
// Combine your two `HttpRoutes` with cats semigroupk syntax
val routes: HttpRoutes[IO] = protectedRoutes <+> publicRoutes Does that work for you? |
Hey @mrdziuban , thanks for the reply. I think the problem is not related to tsec but is the same for http4s' AuthMiddleware. Maybe I did it wrong, but iirc, I had those two options:
Http4s describes that in it's docs: https://http4s.org/v0.23/auth/. The only solution that did work for me was to use a Router. That returned 403 for anything below protected if not authenticated (no matter if there's a matching route or not) and 404 for any requests without a matching route outside of protected. ATM I think my best bet is to give up the idea of a single source of truth and construct routes and reverse routes independently of each other, but utilising a common source of path segments. A method that transforms a given route and a prefix into a new route below that prefix might be helpful, i.e.:
That way, I could still utilise common path and query params, wich would be great (instead of having two locations in code that have to be kept in sync manually). |
I may still be misunderstanding something around your specific use case -- is the tsec middleware (or the generic http4s AuthMiddleware) applied just to If it's the former, then combining If that still doesn't seem like it would work, then I think you could define a method similar to def prefixedAndUnprefixedRoutes[M <: Method, P](method: M, prefix: String)(
f: Route[M, Unit] => Route[M, P]
): (Route[M, P], Route[M, P]) =
(f(method / prefix), f(method))
val (routeForReverseRouting, routeForRouter) =
prefixedAndUnprefixedRoutes(Method.GET, "protected")(_ / "get-route") |
Hi. If you take this code with a http4s AuthMiddleware that never sees an authenticated user: case class User()
val noAuth = AuthMiddleware[F, User](Kleisli{(r: Request[F]) => OptionT.none[F, User]})
val r = HttpRoutes.of[F] {
case GET -> Root / "a" => Ok("a")
} <+> noAuth {
AuthedRoutes.of {
case GET -> Root / "protected" / "b" as user => Ok("b")
}
} <+> HttpRoutes.of[F] {
case GET -> Root / "c" => Ok("c")
} you get:
If you change val noAuth = AuthMiddleware.withFallThrough[F, User](Kleisli{(r: Request[F]) => OptionT.none[F, User]}) you get:
That's what I meant: you either get false 401 or false 404. The middleware needs to be executed in order to find matching routes applied to it. The only way (I could figure out) to execute the middleware only for a given prefix is to use a Router: case class User()
val noAuth = AuthMiddleware[F, User](Kleisli{(r: Request[F]) => OptionT.none[F, User]})
val r = HttpRoutes.of[F] {
case GET -> Root / "a" => Ok("a")
} <+> Router (
"protected" -> noAuth {
AuthedRoutes.of {
case GET -> Root / "b" as user => Ok("b")
}
}
) <+> HttpRoutes.of[F] {
case GET -> Root / "c" => Ok("c")
}
r.orNotFound
Thanks for your suggestion. I'll see, if it fits. Need to get rid of tapir first, which I also tried to use for generating reverse routes (by using a client interpreter). That did not turn out too well. |
Ah I see, thanks for the example. I've been messing around with a bunch of options and can't get the correct behavior using That said, I was able to achieve the correct behavior with some custom logic to match an unauthed request against the protected routes before actually performing auth, and falling back to the public routes if none of the protected routes matched: val publicA = Method.GET / "a"
val publicC = Method.GET / "c"
def protectedRoute[M <: Method, P](method: M)(f: Route[M, Unit] => Route[M, P]): Route[M, P] =
f(method / "protected")
val protectedB = protectedRoute(Method.GET)(_ / "b")
def defineRoutes[F[_], T](authMiddleware: AuthMiddleware[F, T])(
// Routes that require auth, defined so we can match a request before performing auth
authedRoutes: PartialFunction[Request[F], AuthedRequest[F, T] => F[Response[F]]]
)(
// Routes that don't require auth
fallbackRoutes: PartialFunction[Request[F], F[Response[F]]]
)(implicit F: Applicative[F]): HttpRoutes[F] =
Kleisli((req: Request[F]) =>
// Check if any route in `authedRoutes` matches the unauthed request
authedRoutes.lift(req).fold(
// If not, pass through to `fallbackRoutes`
OptionT(fallbackRoutes.lift(req).sequence))(
// If so, call `authMiddleware` and pass the authed request to the route's handler function
f => authMiddleware(Kleisli((authedReq: AuthedRequest[F, T]) => OptionT.liftF(f(authedReq)))).run(req)))
case class User()
val noAuth = AuthMiddleware[IO, User](Kleisli((_: Request[IO]) => OptionT.none[IO, User]))
val routes = defineRoutes[IO, User](noAuth) {
case protectedB(_) => (_: AuthedRequest[IO, User]) => Ok("b")
} {
case publicA(_) => Ok("a")
case publicC(_) => Ok("c")
}
def getResponseCode(path: String): Int =
routes.run(Request[IO](uri = Uri(path = path))).value.unsafeRunSync().fold(404)(_.status.code)
getResponseCode("/a") // 200
getResponseCode("/protected/b") // 401
getResponseCode("/c") // 200
getResponseCode("/d") // 404 It's admittedly kind of hacky, so I wouldn't blame you if you chose another solution, but hopefully it's somewhat helpful. |
Hi,
I really like your project, I'm currently using it inside http4s (0.21). How would reverse routing work if routes are put inside a router? I need to use a router to make my auth-middleware (tsec) not respond with status 401 (Unauthorized) for any requests that are simply not found. I.e.:
Reverse routing obviously does not know about the prefix protected.
A hacky solution would be to rewind the
PathInfoCaret
by the length of the prefix (by a custom middleware) and include the prefix inside the route definitions, but that just doesn't feel right.Any suggestions?
The text was updated successfully, but these errors were encountered: