haskell-servant / haskell-servant/servant

CookieSettings required despite only using JWT Auth

Open
#1,576 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Haskell
Stars
2k
Forks
427
Avg merge
2d 23h
Merged PRs (30d)
5

Description

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:

* We're using JWT access tokens for our authentication, the tokens contain only a signed user ID which we then use to look up the user attributes in a DB
* We don't really have any use for cookie auth or sessions at the moment.

Pursuing this goal, I added the following type:

``` haskell
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:

```haskell
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.

Contributor guide

Open the contributing guide

Research direction

Start by tracing the servant-auth Auth and IsAuth behavior used through hoistServerWithContext, focusing on why CookieSettings and ToJWT are required for JWT-only authentication. Compare that behavior with the AccessTokenClaims and UserByJWT examples; done should clearly establish whether stateless JWT authentication without cookie sessions is supported.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
authentication, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.