git.iko.soy/typeable/servant
on v0.6 help sign in
typeable/servant/doc/tutorial/Authentication.lhs
427 lines 17 kB history raw
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
14authentication scheme.
16## Basic Authentication
18When protecting endpoints with basic authentication, we need to specify two
19items:
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:
27``` haskell ignore
28data BasicAuth (realm :: Symbol) (userData :: *)
29```
31You can use this combinator to protect an API as follows:
33```haskell
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 Control.Monad.Trans.Except (ExceptT, throwE)
48import Data.Aeson (ToJSON)
49import Data.ByteString (ByteString)
50import Data.Map (Map, fromList)
51import Data.Monoid ((<>))
52import qualified Data.Map as Map
53import Data.Proxy (Proxy (Proxy))
54import Data.Text (Text)
55import GHC.Generics (Generic)
56import Network.Wai (Request, requestHeaders)
57import Network.Wai.Handler.Warp (run)
58import Servant.API ((:<|>) ((:<|>)), (:>), BasicAuth,
59 Get, JSON)
60import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
61import Servant.API.Experimental.Auth (AuthProtect)
62import Servant.Server (BasicAuthCheck (BasicAuthCheck),
63 BasicAuthResult( Authorized
64 , Unauthorized
65 ),
66 Context ((:.), EmptyContext),
67 err401, err403, errBody, Server,
68 ServantErr, serveWithContext)
69import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
70 mkAuthHandler)
71import Servant.Server.Experimental.Auth()
73-- | private data that needs protection
74newtype PrivateData = PrivateData { ssshhh :: Text }
75 deriving (Eq, Show, Generic)
77instance ToJSON PrivateData
79-- | public data that anyone can use.
80newtype PublicData = PublicData { somedata :: Text }
81 deriving (Eq, Show, Generic)
83instance ToJSON PublicData
85-- | A user we'll grab from the database when we authenticate someone
86newtype User = User { userName :: Text }
87 deriving (Eq, Show)
89-- | a type to wrap our public api
90type PublicAPI = Get '[JSON] [PublicData]
92-- | a type to wrap our private api
93type PrivateAPI = Get '[JSON] PrivateData
95-- | our API
96type BasicAPI = "public" :> PublicAPI
97 :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
99-- | a value holding a proxy of our API type
100basicAuthApi :: Proxy BasicAPI
101basicAuthApi = Proxy
102```
104You can see that we've prefixed our public API with "public" and our private
105API with "private." Additionally, the private parts of our API use the
106`BasicAuth` combinator to protect them under a Basic Authentication scheme (the
107realm for this authentication is `"foo-realm"`).
109Unfortunately we're not done. When someone makes a request to our `"private"`
110API, we're going to need to provide to servant the logic for validifying
111usernames and passwords. This adds a certain conceptual wrinkle in servant's
112design that we'll briefly discuss. If you want the **TL;DR**: we supply a lookup
113function to servant's new `Context` primitive.
115Until now, all of servant's API combinators extracted information from a request
116or dictated the structure of a response (e.g. a `Capture` param is pulled from
117the request path). Now consider an API resource protected by basic
118authentication. Once the required `WWW-Authenticate` header is checked, we need
119to verify the username and password. But how? One solution would be to force an
120API author to provide a function of type `BasicAuthData -> ExceptT ServantErr IO User`
121and servant should use this function to authenticate a request. Unfortunately
122this didn't work prior to `0.5` because all of servant's machinery was
123engineered around the idea that each combinator can extract information from
124only the request. We cannot extract the function
125`BasicAuthData -> ExceptT ServantErr IO User` from a request! Are we doomed?
127Servant `0.5` introduced `Context` to handle this. The type machinery is beyond
128the scope of this tutorial, but the idea is simple: provide some data to the
129`serve` function, and that data is propagated to the functions that handle each
130combinator. Using `Context`, we can supply a function of type
131`BasicAuthData -> ExceptT ServantErr IO User` to the `BasicAuth` combinator
132handler. This will allow the handler to check authentication and return a `User`
133to downstream handlers if successful.
135In practice we wrap `BasicAuthData -> ExceptT ServantErr IO` into a slightly
136different function to better capture the semantics of basic authentication:
138``` haskell ignore
139-- | The result of authentication/authorization
140data BasicAuthResult usr
141 = Unauthorized
142 | BadPassword
143 | NoSuchUser
144 | Authorized usr
145 deriving (Eq, Show, Read, Generic, Typeable, Functor)
147-- | Datatype wrapping a function used to check authentication.
148newtype BasicAuthCheck usr = BasicAuthCheck
149 { unBasicAuthCheck :: BasicAuthData
150 -> IO (BasicAuthResult usr)
151 }
152 deriving (Generic, Typeable, Functor)
153```
155We now use this datatype to supply servant with a method to authenticate
156requests. In this simple example the only valid username and password is
157`"servant"` and `"server"`, respectively, but in a real, production application
158you might do some database lookup here.
160```haskell
161-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
162authCheck :: BasicAuthCheck User
163authCheck =
164 let check (BasicAuthData username password) =
165 if username == "servant" && password == "server"
166 then return (Authorized (User "servant"))
167 else return Unauthorized
168 in BasicAuthCheck check
169```
171And now we create the `Context` used by servant to find `BasicAuthCheck`:
173```haskell
174-- | We need to supply our handlers with the right Context. In this case,
175-- Basic Authentication requires a Context Entry with the 'BasicAuthCheck' value
176-- tagged with "foo-tag" This context is then supplied to 'server' and threaded
177-- to the BasicAuth HasServer handlers.
178basicAuthServerContext :: Context (BasicAuthCheck User ': '[])
179basicAuthServerContext = authCheck :. EmptyContext
180```
182We're now ready to write our `server` method that will tie everything together:
184```haskell
185-- | an implementation of our server. Here is where we pass all the handlers to our endpoints.
186-- In particular, for the BasicAuth protected handler, we need to supply a function
187-- that takes 'User' as an argument.
188basicAuthServer :: Server BasicAPI
189basicAuthServer =
190 let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
191 privateAPIHandler (user :: User) = return (PrivateData (userName user))
192 in publicAPIHandler :<|> privateAPIHandler
193```
195Finally, our main method and a sample session working with our server:
197```haskell
198-- | hello, server!
199basicAuthMain :: IO ()
200basicAuthMain = run 8080 (serveWithContext basicAuthApi
201 basicAuthServerContext
202 basicAuthServer
203 )
205{- Sample session
207$ curl -XGET localhost:8080/public
208[{"somedata":"foo"},{"somedata":"bar"}
210$ curl -iXGET localhost:8080/private
211HTTP/1.1 401 Unauthorized
212transfer-encoding: chunked
213Date: Thu, 07 Jan 2016 22:36:38 GMT
214Server: Warp/3.1.8
215WWW-Authenticate: Basic realm="foo-realm"
217$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
218HTTP/1.1 200 OK
219transfer-encoding: chunked
220Date: Thu, 07 Jan 2016 22:37:58 GMT
221Server: Warp/3.1.8
222Content-Type: application/json
223{"ssshhh":"servant"}
224-}
225```
227## Generalized Authentication
229Sometimes your server's authentication scheme doesn't quite fit with the
230standards (or perhaps servant hasn't rolled-out support for that new, fancy
231authentication scheme). For such a scenario, servant `0.5` provides easy and
232simple experimental support to roll your own authentication.
234Why experimental? We worked on the design for authentication for a long time. We
235really struggled to find a nice, type-safe niche in the design space. In fact,
236`Context` came out of this work, and while it really fit for schemes like Basic
237and JWT, it wasn't enough to fully support something like OAuth or HMAC, which
238have flows, roles, and other fancy ceremonies. Further, we weren't sure *how*
239people will use auth.
241So, in typical startup fashion, we developed an MVP of 'generalized auth' and
242released it in an experimental module, with the hope of getting feedback from you!
243So, if you're reading this or using generalized auth support, please give us
244your feedback!
246### What is Generalized Authentication?
248**TL;DR**: you throw a tagged `AuthProtect` combinator in front of the endpoints
249you want protected and then supply a function `Request -> ExceptT IO ServantErr user`
250which we run anytime a request matches a protected endpoint. It precisely solves
251the "I just need to protect these endpoints with a function that does some
252complicated business logic" and nothing more. Behind the scenes we use a type
253family instance (`AuthServerData`) and `Context` to accomplish this.
255### Generalized Authentication in Action
257Let's implement a trivial authentication scheme. We will protect our API by
258looking for a cookie named `"servant-auth-cookie"`. This cookie's value will
259contain a key from which we can lookup a `User`.
261```haskell
262-- | A user type that we "fetch from the database" after
263-- performing authentication
264newtype Account = Account { unAccount :: Text }
266-- | A (pure) database mapping keys to users.
267database :: Map ByteString Account
268database = fromList [ ("key1", Account "Anne Briggs")
269 , ("key2", Account "Bruce Cockburn")
270 , ("key3", Account "Ghédalia Tazartès")
271 ]
273-- | A method that, when given a password, will return a Account.
274-- This is our bespoke (and bad) authentication logic.
275lookupAccount :: ByteString -> ExceptT ServantErr IO Account
276lookupAccount key = case Map.lookup key database of
277 Nothing -> throwE (err403 { errBody = "Invalid Cookie" })
278 Just usr -> return usr
279```
281For generalized authentication, servant exposes the `AuthHandler` type,
282which is used to wrap the `Request -> ExceptT IO ServantErr user` logic. Let's
283create a value of type `AuthHandler Request Account` using the above `lookupAccount`
284method:
286```haskell
287-- | The auth handler wraps a function from Request -> ExceptT ServantErr IO Account
288-- we look for a Cookie and pass the value of the cookie to `lookupAccount`.
289authHandler :: AuthHandler Request Account
290authHandler =
291 let handler req = case lookup "servant-auth-cookie" (requestHeaders req) of
292 Nothing -> throwE (err401 { errBody = "Missing auth header" })
293 Just authCookieKey -> lookupAccount authCookieKey
294 in mkAuthHandler handler
295```
297Let's now protect our API with our new, bespoke authentication scheme. We'll
298re-use the endpoints from our Basic Authentication example.
300```haskell
301-- | Our API, with auth-protection
302type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
303 :<|> "public" :> PublicAPI
305-- | A value holding our type-level API
306genAuthAPI :: Proxy AuthGenAPI
307genAuthAPI = Proxy
308```
310Now we need to bring everything together for the server. We have the
311`AuthHandler Request Account` value and an `AuthProtected` endpoint. To bind these
312together, we need to provide a [Type Family](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-families.html)
313instance that tells the `HasServer` instance that our `Context` will supply a
314`Account` (via `AuthHandler Request Account`) and that downstream combinators will
315have access to this `Account` value (or an error will be thrown if authentication
316fails).
318```haskell
320-- | We need to specify the data returned after authentication
321type instance AuthServerData (AuthProtect "cookie-auth") = Account
322```
324Note that we specify the type-level tag `"cookie-auth"` when defining the type
325family instance. This allows us to have multiple authentication schemes
326protecting a single API.
328We now construct the `Context` for our server, allowing us to instantiate a
329value of type `Server AuthGenAPI`, in addition to the server value:
331```haskell
332-- | The context that will be made available to request handlers. We supply the
333-- "cookie-auth"-tagged request handler defined above, so that the 'HasServer' instance
334-- of 'AuthProtect' can extract the handler and run it on the request.
335genAuthServerContext :: Context (AuthHandler Request Account ': '[])
336genAuthServerContext = authHandler :. EmptyContext
338-- | Our API, where we provide all the author-supplied handlers for each end
339-- point. Note that 'privateDataFunc' is a function that takes 'Account' as an
340-- argument. We dont' worry about the authentication instrumentation here,
341-- that is taken care of by supplying context
342genAuthServer :: Server AuthGenAPI
343genAuthServer =
344 let privateDataFunc (Account name) =
345 return (PrivateData ("this is a secret: " <> name))
346 publicData = return [PublicData "this is a public piece of data"]
347 in privateDataFunc :<|> publicData
348```
350We're now ready to start our server (and provide a sample session)!
352```haskell
353-- | run our server
354genAuthMain :: IO ()
355genAuthMain = run 8080 (serveWithContext genAuthAPI genAuthServerContext genAuthServer)
357{- Sample Session:
359$ curl -XGET localhost:8080/private
360Missing auth header
362$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
363[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
365$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
366Invalid Cookie
368$ curl -XGET localhost:8080/public
369[{"somedata":"this is a public piece of data"}]
370-}
371```
373### Recap
375Creating a generalized, ad-hoc authentication scheme was fairly straight
376forward:
3781. use the `AuthProtect` combinator to protect your API.
3792. choose a application-specific data type used by your server when
380authentication is successful (in our case this was `User`).
3813. Create a value of `AuthHandler Request User` which encapsulates the
382authentication logic (`Request -> ExceptT IO ServantErr User`). This function
383will be executed everytime a request matches a protected route.
3844. Provide an instance of the `AuthServerData` type family, specifying your
385application-specific data type returned when authentication is successful (in
386our case this was `User`).
388Caveats:
3901. The module `Servant.Server.Experimental.Auth` contains an orphan `HasServer`
391instance for the `AuthProtect` combinator. You may be get orphan instance
392warnings when using this.
3932. Generalized authentication requires the `UndecidableInstances` extension.
395## Client-side Authentication
397### Basic Authentication
399As of `0.5`, *servant-client* comes with support for basic authentication!
400Endpoints protected by Basic Authentication will require a value of type
401`BasicAuthData` to complete the request.
403### Generalized Authentication
405Servant `0.5` also shipped with support for generalized authentication. Similar
406to the server-side support, clients need to supply an instance of the
407`AuthClientData` type family specifying the datatype the client will use to
408marshal an unauthenticated request into an authenticated request. Generally,
409this will look like:
411```haskell ignore
412-- | The datatype we'll use to authenticate a request. If we were wrapping
413-- something like OAuth, this might be a Bearer token.
414type instance AuthClientData (AuthProtect "cookie-auth") = String
416-- | A method to authenticate a request
417authenticateReq :: String -> Req -> Req
418authenticateReq s req = SCR.addHeader "my-bespoke-header" s req
419```
421Now, if the client method for our protected endpoint was `getProtected`, then
422we could perform authenticated requests as follows:
424```haskell ignore
425-- | one could curry this to make it simpler to work with.
426result = runExceptT (getProtected (mkAuthenticateReq "secret" authenticateReq))
427```