This is a little exercise and experiment to understand how to use the Servant framework for typed web APIs and cookie authentication by means of the Servant.Auth Package, in combination with the ReaderT/IO design pattern as implemented in the RIO Monad.
By default, Servant uses its own Handler
monad, which is a wrapper around ExceptT ServerError IO
.
This is the ExceptT IO anti-pattern, which I want to avoid.
The Servant documentation provides functions to hoist a custom monad into Servant's Handler
; and example using ReaderT is available here.
As of version 0.18.2, Servant supports HTTP basic authentication and a generic extension mechanism for other authentication schemes.
The Servant.Auth Package provides token and cookie-based authentication.
When serving a protected API, an authentication function must be provided as server context.
When using a custom handler monad, the context must be hoisted together with the custom monad: Instead of hoistServer
, Servant provides the function hoistServerWithContext
to this end.
In the present minimal example, I came across two problems, with solutions by helpful comunnity members:
- For a complex API with multiple endpoints combined via Servant's
:<|>
type combinator, the return type is a combination of the individual endpoint return types. If instead of returning a value the authentication check throws an error, this must also be a combination of errors, one for each endpoint. Servant provides the throwAll helper function (from the ThrowAll type class). With RIO as handler monad, the helper function does not work because RIO does not have an instance of theMonadError
typeclass. To alleviate this problem, danidiaz provided theRIOThworAll
type class with therioThrowAll
helper, as discussed in this post on StackOverflow. - Because of some limitation in the Servant base package (see issue #1267), Servant-Auth currently cannot return headers for a HTTP 204 (No Content) response (see servant-auth issue #177). Authentication requires Cookie and X-CSRF headers, though. A workaround suggested in the above issue is to provide an instance for the particular NoContent verbs needed, as discussed in this StackOverflow post.
In putting together the example, I examined the following blog posts, code snippets, and documentation:
- Servant documentation on using a custom handler monad.
- An example on how to hoist a ReaderT/IO monad stack as custom handler.
- A blog post on how to avoid the ExceptT anti-pattern in Servant; note that the blog targets older Servant versions, where the
hoistServer
function was not yet available. - Servant's generalized authentication interface.
- The authentication examples from Servant.Auth.
- An example on how to configure authentication settings - XSRF cookie settings in particular.
- Another cookie authentication example.