haskell-servant / servant

Servant is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.83k stars 413 forks source link

CookieSettings required despite only using JWT Auth #1576

Open ChrisPenner opened 2 years ago

ChrisPenner commented 2 years ago

Hi all 👋🏼

I've been setting up auth for my server using servant-auth and servant-auth-server I've run into a few different issues, first for a bit of context:

Pursuing this goal, I added the following type:

data AccessTokenClaims = AccessTokenClaims
  { userID :: UserId,
    scope :: Scopes
  }

instance FromJWT AccessTokenClaims where
  decodeJWT :: ClaimsSet -> Either Text AccessTokenClaims
  decodeJWT claims = do

    userID <- fromMaybe (Left "Invalid sub") (claims ^? claimSub . _Just . JWT.string . to IDs.fromText)
    pure (AccessTokenClaims {..})
    where
      resultEither = \case
        Aeson.Error err -> Left (Text.pack err)
        Aeson.Success a -> Right a

Now I use Auth '[JWT] AccessTokenClaims in my API routes, but my call to hoistServerWithContext complains:

    • No instance for (HasContextEntry '[] CookieSettings)
        arising from a use of ‘hoistServerWithContext’

Which is strange because I'm not reading or setting cookies anywhere, nor do I intend to.

I've read elsewhere that servant-auth sets session cookies for all users regardless of whether you want it to, and as a result ALSO requires a ToJWT instance on anything that Auth returns. This is at worst an annoyance for AccessTokenClaims, but I proceeded to add a custom combinator which reads and validates the jwt, and then loads the user from our DB:

type AuthedUser = Auth '[UserByJWT] User

data UserByJWT

instance IsAuth UserByJWT User where
  type AuthArgs UserByJWT = '[PG.Connection, JWTSettings]
  runAuth _ _ conn jwtSettings = do
    AccessTokenClaims {userID} <- jwtAuthCheck jwtSettings
    mayUser <- liftIO $ runReaderT (PG.userByUserId userID) conn
    maybe (fail "User not found") pure mayUser

The idea here is to allow loading the authenticated user for all requests which need it.

However once again I'm told that I need ToJWT for User, which implies that Auth wants to save User in a session cookie. I don't want the User to be saved in a session cookie, not only is it potentially large, but it may hold sensitive information that shouldn't leave the server.

It seems like this isn't how Auth is intended to be used, but simultaneously seems like the most sensible way to load an authenticated user safely.

I suspect I could write my own combinator to do this which circumvents servant-auth entirely, or I can just load the user inside every endpoint which requires it; but figured I'd ask whether this is something servant-auth supports somehow first.

gdeest commented 2 years ago

The HasServer instance for Auth does depend on cookie settings being present in the context anyhow:

https://hackage.haskell.org/package/servant-auth-server-0.4.7.0/docs/Servant-Auth-Server.html#t:HasServer

To be clear, I do think it is a design wart: the instance should be independent on the authentication scheme. There is a draft PR that intends to solve this problem:

https://github.com/haskell-servant/servant/pull/1560

ChrisPenner commented 2 years ago

Awesome, glad to see there's a PR in the works. For now I guess I'll use one of the work-arounds I mentioned 👍🏼