1# Authentication in Servant
3Once you've established the basic routes and semantics of your API, it's time
4to consider protecting parts of it. Authentication and authorization are broad
5and nuanced topics; as servant began to explore this space we started small
6with one of HTTP's earliest authentication schemes: [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
8Servant `0.5` shipped with out-of-the-box support for Basic Authentication.
9However, we recognize that every web application is its own beautiful snowflake
10and are offering experimental support for generalized or ad-hoc authentication.
12In this tutorial we'll build two APIs. One protecting certain routes with Basic
13Authentication and another protecting the same routes with a custom, in-house
16## Basic Authentication
18When protecting endpoints with basic authentication, we need to specify two
211. The **realm** of authentication as per the Basic Authentication spec.
222. The datatype returned by the server after authentication is verified. This
23is usually a `User` or `Customer` datatype.
25With those two items in mind, *servant* provides the following combinator:
28data BasicAuth (realm :: Symbol) (userData :: *)
31You 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 #-}
45module Authentication where
47import Data.Aeson (ToJSON)
48import Data.ByteString (ByteString)
49import Data.Map (Map, fromList)
50import Data.Monoid ((<>))
51import qualified Data.Map as Map
52import Data.Proxy (Proxy (Proxy))
53import Data.Text (Text)
54import GHC.Generics (Generic)
55import Network.Wai (Request, requestHeaders)
56import Network.Wai.Handler.Warp (run)
57import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth,
59import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
60import Servant.API.Experimental.Auth (AuthProtect)
61import Servant (throwError)
62import Servant.Server (BasicAuthCheck (BasicAuthCheck),
63 BasicAuthResult( Authorized
66 Context ((:.), EmptyContext),
67 err401, err403, errBody, Server,
68 serveWithContext, Handler)
69import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
71import Servant.Server.Experimental.Auth()
72import Web.Cookie (parseCookies)
74-- | private data that needs protection
75newtype PrivateData = PrivateData { ssshhh :: Text }
76 deriving (Eq, Show, Generic)
78instance ToJSON PrivateData
80-- | public data that anyone can use.
81newtype PublicData = PublicData { somedata :: Text }
82 deriving (Eq, Show, Generic)
84instance ToJSON PublicData
86-- | A user we'll grab from the database when we authenticate someone
87newtype User = User { userName :: Text }
90-- | a type to wrap our public api
91type PublicAPI = Get '[JSON] [PublicData]
93-- | a type to wrap our private api
94type PrivateAPI = Get '[JSON] PrivateData
97type BasicAPI = "public" :> PublicAPI
98 :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
100-- | a value holding a proxy of our API type
101basicAuthApi :: Proxy BasicAPI
105You can see that we've prefixed our public API with "public" and our private
106API with "private." Additionally, the private parts of our API use the
107`BasicAuth` combinator to protect them under a Basic Authentication scheme (the
108realm for this authentication is `"foo-realm"`).
110Unfortunately we're not done. When someone makes a request to our `"private"`
111API, we're going to need to provide to servant the logic for validifying
112usernames and passwords. This adds a certain conceptual wrinkle in servant's
113design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
114function to servant's new `Context` primitive.
116Until now, all of servant's API combinators extracted information from a request
117or dictated the structure of a response (e.g. a `Capture` param is pulled from
118the request path). Now consider an API resource protected by basic
119authentication. Once the required `WWW-Authenticate` header is checked, we need
120to verify the username and password. But how? One solution would be to force an
121API author to provide a function of type `BasicAuthData -> Handler User`
122and servant should use this function to authenticate a request. Unfortunately
123this didn't work prior to `0.5` because all of servant's machinery was
124engineered around the idea that each combinator can extract information from
125only the request. We cannot extract the function
126`BasicAuthData -> Handler User` from a request! Are we doomed?
128Servant `0.5` introduced `Context` to handle this. The type machinery is beyond
129the scope of this tutorial, but the idea is simple: provide some data to the
130`serve` function, and that data is propagated to the functions that handle each
131combinator. Using `Context`, we can supply a function of type
132`BasicAuthData -> Handler User` to the `BasicAuth` combinator
133handler. This will allow the handler to check authentication and return a `User`
134to downstream handlers if successful.
136In practice we wrap `BasicAuthData -> Handler` into a slightly
137different function to better capture the semantics of basic authentication:
140-- | The result of authentication/authorization
141data BasicAuthResult usr
146 deriving (Eq, Show, Read, Generic, Typeable, Functor)
148-- | Datatype wrapping a function used to check authentication.
149newtype BasicAuthCheck usr = BasicAuthCheck
150 { unBasicAuthCheck :: BasicAuthData
151 -> IO (BasicAuthResult usr)
153 deriving (Generic, Typeable, Functor)
156We now use this datatype to supply servant with a method to authenticate
157requests. In this simple example the only valid username and password is
158`"servant"` and `"server"`, respectively, but in a real, production application
159you might do some database lookup here.
162-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
163authCheck :: BasicAuthCheck User
165 let check (BasicAuthData username password) =
166 if username == "servant" && password == "server"
167 then return (Authorized (User "servant"))
168 else return Unauthorized
169 in BasicAuthCheck check
172And now we create the `Context` used by servant to find `BasicAuthCheck`:
175-- | We need to supply our handlers with the right Context. In this case,
176-- Basic Authentication requires a Context Entry with the 'BasicAuthCheck' value
177-- tagged with "foo-tag" This context is then supplied to 'server' and threaded
178-- to the BasicAuth HasServer handlers.
179basicAuthServerContext :: Context (BasicAuthCheck User ': '[])
180basicAuthServerContext = authCheck :. EmptyContext
183We're now ready to write our `server` method that will tie everything together:
186-- | an implementation of our server. Here is where we pass all the handlers to our endpoints.
187-- In particular, for the BasicAuth protected handler, we need to supply a function
188-- that takes 'User' as an argument.
189basicAuthServer :: Server BasicAPI
191 let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
192 privateAPIHandler (user :: User) = return (PrivateData (userName user))
193 in publicAPIHandler :<|> privateAPIHandler
196Finally, our main method and a sample session working with our server:
200basicAuthMain :: IO ()
201basicAuthMain = run 8080 (serveWithContext basicAuthApi
202 basicAuthServerContext
208$ curl -XGET localhost:8080/public
209[{"somedata":"foo"},{"somedata":"bar"}
211$ curl -iXGET localhost:8080/private
212HTTP/1.1 401 Unauthorized
213transfer-encoding: chunked
214Date: Thu, 07 Jan 2016 22:36:38 GMT
216WWW-Authenticate: Basic realm="foo-realm"
218$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
220transfer-encoding: chunked
221Date: Thu, 07 Jan 2016 22:37:58 GMT
223Content-Type: application/json
228## Generalized Authentication
230Sometimes your server's authentication scheme doesn't quite fit with the
231standards (or perhaps servant hasn't rolled-out support for that new, fancy
232authentication scheme). For such a scenario, servant `0.5` provides easy and
233simple experimental support to roll your own authentication.
235Why experimental? We worked on the design for authentication for a long time. We
236really struggled to find a nice, type-safe niche in the design space. In fact,
237`Context` came out of this work, and while it really fit for schemes like Basic
238and JWT, it wasn't enough to fully support something like OAuth or HMAC, which
239have flows, roles, and other fancy ceremonies. Further, we weren't sure *how*
242So, in typical startup fashion, we developed an MVP of 'generalized auth' and
243released it in an experimental module, with the hope of getting feedback from you!
244So, if you're reading this or using generalized auth support, please give us
247### What is Generalized Authentication?
249**TL;DR**: you throw a tagged `AuthProtect` combinator in front of the
250endpoints you want protected and then supply a function `Request -> Handler a`,
251where `a` is the type of your choice representing the data returned by
252successful authentication - e.g., a `User` or, in our example below, `Account`.
253This function is run anytime a request matches a protected endpoint. It
254precisely solves the "I just need to protect these endpoints with a function
255that does some complicated business logic" and nothing more. Behind the scenes
256we use a type family instance (`AuthServerData`) and `Context` to accomplish
259### Generalized Authentication in Action
261Let's implement a trivial authentication scheme. We will protect our API by
262looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
263contain a key from which we can lookup a `Account`.
266-- | An account type that we "fetch from the database" after
267-- performing authentication
268newtype Account = Account { unAccount :: Text }
270-- | A (pure) database mapping keys to accounts.
271database :: Map ByteString Account
272database = fromList [ ("key1", Account "Anne Briggs")
273 , ("key2", Account "Bruce Cockburn")
274 , ("key3", Account "Ghédalia Tazartès")
277-- | A method that, when given a password, will return a Account.
278-- This is our bespoke (and bad) authentication logic.
279lookupAccount :: ByteString -> Handler Account
280lookupAccount key = case Map.lookup key database of
281 Nothing -> throwError (err403 { errBody = "Invalid Cookie" })
282 Just usr -> return usr
285For generalized authentication, servant exposes the `AuthHandler` type,
286which is used to wrap the `Request -> Handler Account` logic. Let's
287create a value of type `AuthHandler Request Account` using the above `lookupAccount`
288method (note: we depend upon [`cookie`](https://hackage.haskell.org/package/cookie)'s
289`parseCookies` for this):
293--- | The auth handler wraps a function from Request -> Handler Account.
294--- We look for a token in the request headers that we expect to be in the cookie.
295--- The token is then passed to our `lookupAccount` function.
296authHandler :: AuthHandler Request Account
297authHandler = mkAuthHandler handler
299 maybeToEither e = maybe (Left e) Right
300 throw401 msg = throwError $ err401 { errBody = msg }
301 handler req = either throw401 lookupAccount $ do
302 cookie <- maybeToEither "Missing cookie header" $ lookup "cookie" $ requestHeaders req
303 maybeToEither "Missing token in cookie" $ lookup "servant-auth-cookie" $ parseCookies cookie
306Let's now protect our API with our new, bespoke authentication scheme. We'll
307re-use the endpoints from our Basic Authentication example.
310-- | Our API, with auth-protection
311type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
312 :<|> "public" :> PublicAPI
314-- | A value holding our type-level API
315genAuthAPI :: Proxy AuthGenAPI
319Now we need to bring everything together for the server. We have the
320`AuthHandler Request Account` value and an `AuthProtected` endpoint. To bind these
321together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
322instance that tells the `HasServer` instance that our `Context` will supply a
323`Account` (via `AuthHandler Request Account`) and that downstream combinators will
324have access to this `Account` value (or an error will be thrown if authentication
329-- | We need to specify the data returned after authentication
330type instance AuthServerData (AuthProtect "cookie-auth") = Account
333Note that we specify the type-level tag `"cookie-auth"` when defining the type
334family instance. This allows us to have multiple authentication schemes
335protecting a single API.
337We now construct the `Context` for our server, allowing us to instantiate a
338value of type `Server AuthGenAPI`, in addition to the server value:
341-- | The context that will be made available to request handlers. We supply the
342-- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
343-- of 'AuthProtect' can extract the handler and run it on the request.
344genAuthServerContext :: Context (AuthHandler Request Account ': '[])
345genAuthServerContext = authHandler :. EmptyContext
347-- | Our API, where we provide all the author-supplied handlers for each end
348-- point. Note that 'privateDataFunc' is a function that takes 'Account' as an
349-- argument. We dont' worry about the authentication instrumentation here,
350-- that is taken care of by supplying context
351genAuthServer :: Server AuthGenAPI
353 let privateDataFunc (Account name) =
354 return (PrivateData ("this is a secret: " <> name))
355 publicData = return [PublicData "this is a public piece of data"]
356 in privateDataFunc :<|> publicData
359We're now ready to start our server (and provide a sample session)!
364genAuthMain = run 8080 (serveWithContext genAuthAPI genAuthServerContext genAuthServer)
368$ curl -XGET localhost:8080/private
371$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
372[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
374$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
377$ curl -XGET localhost:8080/public
378[{"somedata":"this is a public piece of data"}]
384Creating a generalized, ad-hoc authentication scheme was fairly straight
3871. use the `AuthProtect` combinator to protect your API.
3882. choose a application-specific data type used by your server when
389authentication is successful (in our case this was `Account`).
3903. Create a value of `AuthHandler Request Account` which encapsulates the
391authentication logic (`Request -> Handler Account`). This function
392will be executed everytime a request matches a protected route.
3934. Provide an instance of the `AuthServerData` type family, specifying your
394application-specific data type returned when authentication is successful (in
395our case this was `Account`).
3991. The module `Servant.Server.Experimental.Auth` contains an orphan `HasServer`
400instance for the `AuthProtect` combinator. You may be get orphan instance
401warnings when using this.
4022. Generalized authentication requires the `UndecidableInstances` extension.
404## Client-side Authentication
406### Basic Authentication
408As of `0.5`, *servant-client* comes with support for basic authentication!
409Endpoints protected by Basic Authentication will require a value of type
410`BasicAuthData` to complete the request.
412You can find more comprehensive Basic Authentication example in the [Cookbook](../cookbook/basic-auth/BasicAuth.html).
414### Generalized Authentication
416Servant `0.5` also shipped with support for generalized authentication. Similar
417to the server-side support, clients need to supply an instance of the
418`AuthClientData` type family specifying the datatype the client will use to
419marshal an unauthenticated request into an authenticated request. Generally,
423import Servant.Common.Req (Req, addHeader)
425-- | The datatype we'll use to authenticate a request. If we were wrapping
426-- something like OAuth, this might be a Bearer token.
427type instance AuthClientData (AuthProtect "cookie-auth") = String
429-- | A method to authenticate a request
430authenticateReq :: String -> Req -> Req
431authenticateReq s req = addHeader "my-bespoke-header" s req
434Now, if the client method for our protected endpoint was `getProtected`, then
435we could perform authenticated requests as follows:
438-- | one could curry this to make it simpler to work with.
439result = runExceptT (getProtected (mkAuthenticateReq "secret" authenticateReq))
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list