@@ -0,0 +1,417 @@
1+# Authentication in Servant
3+Once you've established the basic routes and semantics of your API, it's time
4+to consider protecting parts of it. Authentication and authorization are broad
5+and nuanced topics; as servant began to explore this space we started small
6+with one of HTTP's earliest authentication schemes: [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
8+Servant `0.5` shipped with out-of-the-box support for Basic Authentication.
9+However, we recognize that every web application is its own beautiful snowflake
10+and are offering experimental support for generalized or ad-hoc authentication.
12+In this tutorial we'll build two APIs. One protecting certain routes with Basic
13+Authentication and another protecting the same routes with a custom, in-house
14+authentication scheme.
16+## Basic Authentication
18+When protecting endpoints with basic authentication, we need to specify two
21+1. The **realm** of authentication as per the Basic Authentication spec.
22+2. The datatype returned by the server after authentication is verified. This
23+is usually a `User` or `Customer` datatype.
25+With those two items in mind, *servant* provides the following combinator:
28+data BasicAuth (realm :: Symbol) (userData :: *)
31+You can use this combinator to protect an API as follows:
34+{-# LANGUAGE DataKinds #-}
35+{-# LANGUAGE DeriveGeneric #-}
36+{-# LANGUAGE FlexibleContexts #-}
37+{-# LANGUAGE FlexibleInstances #-}
38+{-# LANGUAGE MultiParamTypeClasses #-}
39+{-# LANGUAGE OverloadedStrings #-}
40+{-# LANGUAGE ScopedTypeVariables #-}
41+{-# LANGUAGE TypeFamilies #-}
42+{-# LANGUAGE TypeOperators #-}
43+{-# LANGUAGE UndecidableInstances #-}
45+module Authentication where
47+import Data.Aeson (ToJSON)
48+import Data.Proxy (Proxy (Proxy))
49+import Data.Text (Text)
50+import GHC.Generics (Generic)
51+import Network.Wai.Handler.Warp (run)
52+import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth,
54+import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
55+import Servant.API.Experimental (AuthProtect)
56+import Servant.Server (BasicAuthCheck (BasicAuthCheck),
57+ BasicAuthResult( Authorized
60+ Context ((:.), EmptyContext), Server,
62+import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
64+import Servant.Server.Experimenta.Auth()
66+- | private data that needs protection
67+newtype PrivateData = PrivateData { ssshhh :: Text }
68+ deriving (Eq, Show, Generic)
70+instance ToJSON PrivateData
72+-- | public data that anyone can use.
73+newtype PublicData = PublicData { somedata :: Text }
74+ deriving (Eq, Show, Generic)
76+instance ToJSON PublicData
78+-- | A user we'll grab from the database when we authenticate someone
79+newtype User = User { userName :: Text }
82+-- | a type to wrap our public api
83+type PublicAPI = Get '[JSON] [PublicData]
85+-- | a type to wrap our private api
86+type PrivateAPI = Get '[JSON] PrivateData
89+type BasicAPI = "public" :> PublicAPI
90+ :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
92+-- | a value holding a proxy of our API type
93+basicAuthApi :: Proxy BasicAPI
97+You can see that we've prefixed our public API with "public" and our private
98+API with "private." Additionally, the private parts of our API use the
99+`BasicAuth` combinator to protect them under a Basic Authentication scheme (the
100+realm for this authentication is `"foo-realm"`).
102+Unfortunately we're not done. When someone makes a request to our `"private"`
103+API, we're going to need to provide to servant the logic for validifying
104+usernames and passwords. This adds a certain conceptual wrinkle in servant's
105+design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
106+function to servant's new `Context` primitive.
108+Until now, all of servant's API combinators extracted information from a request
109+or dictated the structure of a response (e.g. a `Capture` param is pulled from
110+the request path). Now consider an API resource protected by basic
111+authentication. Once the required `WWW-Authenticate` header is checked, we need
112+to verify the username and password. But how? One solution would be to force an
113+API author to provide a function of type `BasicAuthData -> ExceptT ServantErr IO User`
114+and servant should use this function to authenticate a request. Unfortunately
115+this didn't work prior to `0.5` because all of servant's machinery was
116+engineered around the idea that each combinator can extract information from
117+only the request. We cannot extract the function
118+`BasicAuthData -> ExceptT ServantErr IO User` from a request! Are we doomed?
120+Servant `0.5` introduced `Context` to handle this. The type machinery is beyond
121+the scope of this tutorial, but the idea is simple: provide some data to the
122+`serve` function, and that data is propagated to the functions that handle each
123+combinator. Using `Context`, we can supply a function of type
124+`BasicAuthData -> ExceptT ServantErr IO User` to the `BasicAuth` combinator
125+handler. This will allow the handler to check authentication and return a `User`
126+to downstream handlers if successful.
128+In practice we wrap `BasicAuthData -> ExceptT ServantErr IO` into a slightly
129+different function to better capture the semantics of basic authentication:
132+-- | The result of authentication/authorization
133+data BasicAuthResult usr
138+ deriving (Eq, Show, Read, Generic, Typeable, Functor)
140+-- | Datatype wrapping a function used to check authentication.
141+newtype BasicAuthCheck usr = BasicAuthCheck
142+ { unBasicAuthCheck :: BasicAuthData
143+ -> IO (BasicAuthResult usr)
145+ deriving (Generic, Typeable, Functor)
148+We now use this datatype to supply servant with a method to authenticate
149+requests. In this simple example the only valid username and password is
150+`"servant"` and `"server"`, respectively, but in a real, production application
151+you might do some database lookup here.
154+-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
155+authCheck :: BasicAuthCheck User
157+ let check (BasicAuthData username password) =
158+ if username == "servant" && password == "server"
159+ then return (Authorized (User "servant"))
160+ else return Unauthorized
161+ in BasicAuthCheck check
164+And now we create the `Context` used by servant to find `BasicAuthCheck`:
167+-- | We need to supply our handlers with the right Context. In this case,
168+-- Basic Authentication requires a Context Entry with the 'BasicAuthCheck' value
169+-- tagged with "foo-tag" This context is then supplied to 'server' and threaded
170+-- to the BasicAuth HasServer handlers.
171+serverContext :: Context (BasicAuthCheck User ': '[])
172+serverContext = authCheck :. EmptyContext
175+We're now ready to write our `server` method that will tie everything together:
178+-- | an implementation of our server. Here is where we pass all the handlers to our endpoints.
179+-- In particular, for the BasicAuth protected handler, we need to supply a function
180+-- that takes 'User' as an argument.
181+server :: Server BasicAPI
183+ let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
184+ privateAPIHandler (user :: User) = return (PrivateData (userName user))
185+ in publicAPIHandler :<|> privateAPIHandler
188+Finally, our main method and a sample session working with our server:
192+basicAuthMain :: IO ()
193+basicAuthMain = run 8080 (serveWithContext basicAuthApi serverContext server)
197+$ curl -XGET localhost:8080/public
198+[{"somedata":"foo"},{"somedata":"bar"}
200+$ curl -iXGET localhost:8080/private
201+HTTP/1.1 401 Unauthorized
202+transfer-encoding: chunked
203+Date: Thu, 07 Jan 2016 22:36:38 GMT
205+WWW-Authenticate: Basic realm="foo-realm"
207+$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
209+transfer-encoding: chunked
210+Date: Thu, 07 Jan 2016 22:37:58 GMT
212+Content-Type: application/json
217+## Generalized Authentication
219+Sometimes your server's authentication scheme doesn't quite fit with the
220+standards (or perhaps servant hasn't rolled-out support for that new, fancy
221+authentication scheme). For such a scenario, servant `0.5` provides easy and
222+simple experimental support to roll your own authentication.
224+Why experimental? We worked on the design for authentication for a long time. We
225+really struggled to find a nice, type-safe niche in the design space. In fact,
226+`Context` came out of this work, and while it really fit for schemes like Basic
227+and JWT, it wasn't enough to fully support something like OAuth or HMAC, which
228+have flows, roles, and other fancy ceremonies. Further, we weren't sure *how*
229+people will use auth.
231+So, in typical startup fashion, we developed an MVP of 'generalized auth' and
232+released it in an experimental module, with the hope of getting feedback from you!
233+So, if you're reading this or using generalized auth support, please give us
236+### What is Generalized Authentication?
238+**TL;DR**: you throw a tagged `AuthProtect` combinator in front of the endpoints
239+you want protected and then supply a function `Request -> ExceptT IO ServantErr user`
240+which we run anytime a request matches a protected endpoint. It precisely solves
241+the "I just need to protect these endpoints with a function that does some
242+complicated business logic" and nothing more. Behind the scenes we use a type
243+family instance (`AuthServerData`) and `Context` to accomplish this.
245+### Generalized Authentication in Action
247+Let's implement a trivial authentication scheme. We will protect our API by
248+looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
249+contain a key from which we can lookup a `User`.
252+-- | A user type that we "fetch from the database" after
253+-- performing authentication
254+newtype User = User { unUser :: Text }
256+-- | A (pure) database mapping keys to users.
257+database :: Map ByteString User
258+database = fromList [ ("key1", User "Anne Briggs")
259+ , ("key2", User "Bruce Cockburn")
260+ , ("key3", User "Ghédalia Tazartès")
263+-- | A method that, when given a password, will return a User.
264+-- This is our bespoke (and bad) authentication logic.
265+lookupUser :: ByteString -> ExceptT ServantErr IO User
266+lookupUser key = case Map.lookup key database of
267+ Nothing -> throwE (err403 { errBody = "Invalid Cookie" })
268+ Just usr -> return usr
271+For generalized authentication, servant exposes the `AuthHandler` type,
272+which is used to wrap the `Request -> ExceptT IO ServantErr user` logic. Let's
273+create a value of type `AuthHandler Request User` using the above `lookupUser`
277+-- | The auth handler wraps a function from Request -> ExceptT ServantErr IO User
278+-- we look for a Cookie and pass the value of the cookie to `lookupUser`.
279+authHandler :: AuthHandler Request User
281+ let handler req = case lookup "servant-auth-cookie" (requestHeaders req) of
282+ Nothing -> throwE (err401 { errBody = "Missing auth header" })
283+ Just authCookieKey -> lookupUser authCookieKey
284+ in mkAuthHandler handler
287+Let's now protect our API with our new, bespoke authentication scheme. We'll
288+re-use the endpoints from our Basic Authentication example.
291+-- | Our API, with auth-protection
292+type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
293+ :<|> "public" :> PublicAPI
295+-- | A value holding our type-level API
296+genAuthApi :: Proxy AuthGenAPI
300+Now we need to bring everything together for the server. We have the
301+`AuthHandler Request User` value and an `AuthProtected` endpoint. To bind these
302+together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
303+instance that tells the `HasServer` instance that our `Context` will supply a
304+`User` (via `AuthHandler Request User`) and that downstream combinators will
305+have access to this `User` value (or an error will be thrown if authentication
310+-- | We need to specify the data returned after authentication
311+type instance AuthServerData (AuthProtect "cookie-auth") = User
314+Note that we specify the type-level tag `"cookie-auth"` when defining the type
315+family instance. This allows us to have multiple authentication schemes
316+protecting a single API.
318+We now construct the `Context` for our server, allowing us to instantiate a
319+value of type `Server AuthGenAPI`, in addition to the server value:
322+-- | The context that will be made available to request handlers. We supply the
323+-- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
324+-- of 'AuthProtect' can extract the handler and run it on the request.
325+serverContext :: Context (AuthHandler Request User ': '[])
326+serverContext = authHandler :. EmptyContext
328+-- | Our API, where we provide all the author-supplied handlers for each end
329+-- point. Note that 'privateDataFunc' is a function that takes 'User' as an
330+-- argument. We dont' worry about the authentication instrumentation here,
331+-- that is taken care of by supplying context
332+server :: Server AuthGenAPI
334+ let privateDataFunc (User name) =
335+ return [PrivateData ("this is a secret: " <> name)]
336+ publicData = [PublicData "this is a public piece of data"]
337+ in privateDataFunc :<|> return publicData
340+We're now ready to start our server (and provide a sample session)!
345+genAuthMain = run 8080 (serveWithContext api serverContext server)
349+$ curl -XGET localhost:8080/private
352+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
353+[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
355+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
358+$ curl -XGET localhost:8080/public
359+[{"somedata":"this is a public piece of data"}]
365+Creating a generalized, ad-hoc authentication scheme was fairly straight
368+1. use the `AuthProtect` combinator to protect your API.
369+2. choose a application-specific data type used by your server when
370+authentication is successful (in our case this was `User`).
371+3. Create a value of `AuthHandler Request User` which encapsulates the
372+authentication logic (`Request -> ExceptT IO ServantErr User`). This function
373+will be executed everytime a request matches a protected route.
374+4. Provide an instance of the `AuthServerData` type family, specifying your
375+application-specific data type returned when authentication is successful (in
376+our case this was `User`).
380+1. The module `Servant.Server.Experimental.Auth` contains an orphan `HasServer`
381+instance for the `AuthProtect` combinator. You may be get orphan instance
382+warnings when using this.
383+2. Generalized authentication requires the `UndecidableInstances` extension.
385+## Client-side Authentication
387+### Basic Authentication
389+As of `0.5`, *servant-client* comes with support for basic authentication!
390+Endpoints protected by Basic Authentication will require a value of type
391+`BasicAuthData` to complete the request.
393+### Generalized Authentication
395+Servant `0.5` also shipped with support for generalized authentication. Similar
396+to the server-side support, clients need to supply an instance of the
397+`AuthClientData` type family specifying the datatype the client will use to
398+marshal an unauthenticated request into an authenticated request. Generally,
402+-- | The datatype we'll use to authenticate a request. If we were wrapping
403+-- something like OAuth, this might be a Bearer token.
404+type instance AuthClientData (AuthProtect "cookie-auth") = String
406+-- | A method to authenticate a request
407+authenticateReq :: String -> Req -> Req
408+authenticateReq s req = SCR.addHeader "my-bespoke-header" s req
411+Now, if the client method for our protected endpoint was `getProtected`, then
412+we could perform authenticated requests as follows:
415+-- | one could curry this to make it simpler to work with.
416+result = runExceptT (getProtected (mkAuthenticateReq "secret" authenticateReq))