Trouble with route selection for endpoints protected by Cookie Auth (Servant)

Hello, I’m having an issue with the behavior of the router that I don’t know how to resolve. I have the following Server definition:

type HomeRoute = HXRequest :> Get '[HTML] (Partial Home)

type UnprotectedRoutes = HomeRoute

type ProtectedRoutes = HomeRoute

type Routes =
  (Auth '[Cookie] Model.User :> ProtectedRoutes)
    :<|> UnprotectedRoutes

As you can see, I have a HomeRoute under both UnprotectedRoutes and ProtectedRoutes. It returns a different home page depending on whether or not the user is logged in. The issue is that the routing mechanism doesn’t seem to differentiate between

Auth '[Cookie] Model.User :> HXRequest :> Get '[HTML] (Partial Home)

and

HXRequest :> Get '[HTML] (Partial Home)

In my opinion, the way this should work is like this:

  1. If the request from the client has the cookie, match against the first route.
  2. If the request doesn’t have a cookie, then match against the second route.

But it doesn’t work like that. It matches against HXRequest :> Get '[HTML] (Partial Home) whether or not the request has a cookie.

This means I can’t represent two pages, one for a non-authenticated users and one for an authenticated users, by the same URL. I want to be able to get to both routes with the same URL baseUrl/. Instead, I must add a path in front of the protected route, like baseUrl/protected/.

This works fine:

Auth '[Cookie] Model.User :> "protected" :> HXRequest :> Get '[HTML] (Partial Home)

Is there a way around this? Maybe my Server types aren’t structured correctly? If it’s not possible I don’t want to waste more time on the problem. It’s just a slight annoyance.

The servant router chooses the first “matching” route. For your routes, the protected one will match first. If there is no cookie present, it will still match the protected route, it will just fail authentication.
The way around it would either be to have a different route, or to have different handler logic, f.e. pattern-matching on the Indefinite constructor of the AuthResult that you get in your handler:

handler auth ... = case auth of
  Authenticated user -> protectedHandler user
  Indefinite -> unprotectedHandler
  _ -> badRequest

It’s a bit more hacky though.

A more complex variant of #2 would be to use servant’s built-in experimental generalized auth.
That gives you complete control over your authentication, and you can pas a custom datatype to your handler indicating whether the auth cookie was valid, invalid, or not present.

2 Likes

Interesting. I guess I’ll look into generalized auth. I’ve seen it before, but didn’t know if that would solve my problem. Thank you for the help!

Edit: Actually, I’ll probably just go with the first solution you suggested for now. Seems to work as I intended. I’ll look into generalized auth another time haha