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.Proxy (Proxy (Proxy))
49import Data.Text (Text)
50import GHC.Generics (Generic)
51import Network.Wai.Handler.Warp (run)
52import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth,
54import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
55import Servant.API.Experimental (AuthProtect)
56import Servant.Server (BasicAuthCheck (BasicAuthCheck),
57 BasicAuthResult( Authorized
60 Context ((:.), EmptyContext), Server,
62import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
64import Servant.Server.Experimenta.Auth()
66- | private data that needs protection
67newtype PrivateData = PrivateData { ssshhh :: Text }
68 deriving (Eq, Show, Generic)
70instance ToJSON PrivateData
72-- | public data that anyone can use.
73newtype PublicData = PublicData { somedata :: Text }
74 deriving (Eq, Show, Generic)
76instance ToJSON PublicData
78-- | A user we'll grab from the database when we authenticate someone
79newtype User = User { userName :: Text }
82-- | a type to wrap our public api
83type PublicAPI = Get '[JSON] [PublicData]
85-- | a type to wrap our private api
86type PrivateAPI = Get '[JSON] PrivateData
89type BasicAPI = "public" :> PublicAPI
90 :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
92-- | a value holding a proxy of our API type
93basicAuthApi :: Proxy BasicAPI
97You can see that we've prefixed our public API with "public" and our private
98API with "private." Additionally, the private parts of our API use the
99`BasicAuth` combinator to protect them under a Basic Authentication scheme (the
100realm for this authentication is `"foo-realm"`).
102Unfortunately we're not done. When someone makes a request to our `"private"`
103API, we're going to need to provide to servant the logic for validifying
104usernames and passwords. This adds a certain conceptual wrinkle in servant's
105design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
106function to servant's new `Context` primitive.
108Until now, all of servant's API combinators extracted information from a request
109or dictated the structure of a response (e.g. a `Capture` param is pulled from
110the request path). Now consider an API resource protected by basic
111authentication. Once the required `WWW-Authenticate` header is checked, we need
112to verify the username and password. But how? One solution would be to force an
113API author to provide a function of type `BasicAuthData -> ExceptT ServantErr IO User`
114and servant should use this function to authenticate a request. Unfortunately
115this didn't work prior to `0.5` because all of servant's machinery was
116engineered around the idea that each combinator can extract information from
117only the request. We cannot extract the function
118`BasicAuthData -> ExceptT ServantErr IO User` from a request! Are we doomed?
120Servant `0.5` introduced `Context` to handle this. The type machinery is beyond
121the 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
123combinator. Using `Context`, we can supply a function of type
124`BasicAuthData -> ExceptT ServantErr IO User` to the `BasicAuth` combinator
125handler. This will allow the handler to check authentication and return a `User`
126to downstream handlers if successful.
128In practice we wrap `BasicAuthData -> ExceptT ServantErr IO` into a slightly
129different function to better capture the semantics of basic authentication:
132-- | The result of authentication/authorization
133data BasicAuthResult usr
138 deriving (Eq, Show, Read, Generic, Typeable, Functor)
140-- | Datatype wrapping a function used to check authentication.
141newtype BasicAuthCheck usr = BasicAuthCheck
142 { unBasicAuthCheck :: BasicAuthData
143 -> IO (BasicAuthResult usr)
145 deriving (Generic, Typeable, Functor)
148We now use this datatype to supply servant with a method to authenticate
149requests. In this simple example the only valid username and password is
150`"servant"` and `"server"`, respectively, but in a real, production application
151you might do some database lookup here.
154-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
155authCheck :: 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
164And 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.
171serverContext :: Context (BasicAuthCheck User ': '[])
172serverContext = authCheck :. EmptyContext
175We'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.
181server :: Server BasicAPI
183 let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
184 privateAPIHandler (user :: User) = return (PrivateData (userName user))
185 in publicAPIHandler :<|> privateAPIHandler
188Finally, our main method and a sample session working with our server:
192basicAuthMain :: IO ()
193basicAuthMain = run 8080 (serveWithContext basicAuthApi serverContext server)
197$ curl -XGET localhost:8080/public
198[{"somedata":"foo"},{"somedata":"bar"}
200$ curl -iXGET localhost:8080/private
201HTTP/1.1 401 Unauthorized
202transfer-encoding: chunked
203Date: Thu, 07 Jan 2016 22:36:38 GMT
205WWW-Authenticate: Basic realm="foo-realm"
207$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
209transfer-encoding: chunked
210Date: Thu, 07 Jan 2016 22:37:58 GMT
212Content-Type: application/json
217## Generalized Authentication
219Sometimes your server's authentication scheme doesn't quite fit with the
220standards (or perhaps servant hasn't rolled-out support for that new, fancy
221authentication scheme). For such a scenario, servant `0.5` provides easy and
222simple experimental support to roll your own authentication.
224Why experimental? We worked on the design for authentication for a long time. We
225really 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
227and JWT, it wasn't enough to fully support something like OAuth or HMAC, which
228have flows, roles, and other fancy ceremonies. Further, we weren't sure *how*
231So, in typical startup fashion, we developed an MVP of 'generalized auth' and
232released it in an experimental module, with the hope of getting feedback from you!
233So, 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
239you want protected and then supply a function `Request -> ExceptT IO ServantErr user`
240which we run anytime a request matches a protected endpoint. It precisely solves
241the "I just need to protect these endpoints with a function that does some
242complicated business logic" and nothing more. Behind the scenes we use a type
243family instance (`AuthServerData`) and `Context` to accomplish this.
245### Generalized Authentication in Action
247Let's implement a trivial authentication scheme. We will protect our API by
248looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
249contain a key from which we can lookup a `User`.
252-- | A user type that we "fetch from the database" after
253-- performing authentication
254newtype User = User { unUser :: Text }
256-- | A (pure) database mapping keys to users.
257database :: Map ByteString User
258database = 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.
265lookupUser :: ByteString -> ExceptT ServantErr IO User
266lookupUser key = case Map.lookup key database of
267 Nothing -> throwE (err403 { errBody = "Invalid Cookie" })
268 Just usr -> return usr
271For generalized authentication, servant exposes the `AuthHandler` type,
272which is used to wrap the `Request -> ExceptT IO ServantErr user` logic. Let's
273create 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`.
279authHandler :: 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
287Let's now protect our API with our new, bespoke authentication scheme. We'll
288re-use the endpoints from our Basic Authentication example.
291-- | Our API, with auth-protection
292type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
293 :<|> "public" :> PublicAPI
295-- | A value holding our type-level API
296genAuthApi :: Proxy AuthGenAPI
300Now we need to bring everything together for the server. We have the
301`AuthHandler Request User` value and an `AuthProtected` endpoint. To bind these
302together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
303instance that tells the `HasServer` instance that our `Context` will supply a
304`User` (via `AuthHandler Request User`) and that downstream combinators will
305have access to this `User` value (or an error will be thrown if authentication
310-- | We need to specify the data returned after authentication
311type instance AuthServerData (AuthProtect "cookie-auth") = User
314Note that we specify the type-level tag `"cookie-auth"` when defining the type
315family instance. This allows us to have multiple authentication schemes
316protecting a single API.
318We now construct the `Context` for our server, allowing us to instantiate a
319value 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.
325serverContext :: Context (AuthHandler Request User ': '[])
326serverContext = 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
332server :: 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
340We're now ready to start our server (and provide a sample session)!
345genAuthMain = 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"}]
365Creating a generalized, ad-hoc authentication scheme was fairly straight
3681. use the `AuthProtect` combinator to protect your API.
3692. choose a application-specific data type used by your server when
370authentication is successful (in our case this was `User`).
3713. Create a value of `AuthHandler Request User` which encapsulates the
372authentication logic (`Request -> ExceptT IO ServantErr User`). This function
373will be executed everytime a request matches a protected route.
3744. Provide an instance of the `AuthServerData` type family, specifying your
375application-specific data type returned when authentication is successful (in
376our case this was `User`).
3801. The module `Servant.Server.Experimental.Auth` contains an orphan `HasServer`
381instance for the `AuthProtect` combinator. You may be get orphan instance
382warnings when using this.
3832. Generalized authentication requires the `UndecidableInstances` extension.
385## Client-side Authentication
387### Basic Authentication
389As of `0.5`, *servant-client* comes with support for basic authentication!
390Endpoints protected by Basic Authentication will require a value of type
391`BasicAuthData` to complete the request.
393### Generalized Authentication
395Servant `0.5` also shipped with support for generalized authentication. Similar
396to the server-side support, clients need to supply an instance of the
397`AuthClientData` type family specifying the datatype the client will use to
398marshal 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.
404type instance AuthClientData (AuthProtect "cookie-auth") = String
406-- | A method to authenticate a request
407authenticateReq :: String -> Req -> Req
408authenticateReq s req = SCR.addHeader "my-bespoke-header" s req
411Now, if the client method for our protected endpoint was `getProtected`, then
412we could perform authenticated requests as follows:
415-- | one could curry this to make it simpler to work with.
416result = 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