git.iko.soy/typeable/servant
on master help sign in
Merge pull request #416 from haskell-servant/jkarni/basic-auth-tutorial

Add basic auth to tutorial

commitfbf7c02d734fbbe049cdc1335a6f099e57610a3e authorAaron Levin <vilevin@gmail.com> dateSun 20 Mar 2016, 22:21 (11 years ago) parent4e37365f742a5c
2 files changed+445
doc/tutorial/ApiType.lhs+28 doc/tutorial/Authentication.lhs+417
doc/tutorial/ApiType.lhs+28
@@ -287,6 +287,34 @@ response, you could write it as below:
287287type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User])
288288```
289289
290+### Basic Authentication
291+
292+Once you've established the basic routes and semantics of your API, it's time
293+to consider protecting parts of it. Authentication and authorization are broad
294+and nuanced topics; as servant began to explore this space we started small
295+with one of HTTP's earliest authentication schemes: [Basic
296+Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
297+
298+When protecting endpoints with basic authentication, we need to specify two items:
299+
300+1. The **realm** of authentication as per the Basic Authentictaion spec.
301+2. The datatype returned by the server after authentication is verified. This
302+ is usually a `User` or `Customer` type datatype.
303+
304+With those two items in mind, *servant* provides the following combinator:
305+
306+``` haskell ignore
307+data BasicAuth (realm :: Symbol) (userData :: *)
308+```
309+
310+Which is used like so:
311+
312+``` haskell
313+type ProtectedAPI12
314+ = UserAPI -- this is public
315+ :<|> BasicAuth "my-real" User :> UserAPI2 -- this is protected by auth
316+```
317+
290318### Interoperability with `wai`: `Raw`
291319
292320Finally, we also include a combinator named `Raw` that provides an escape hatch
doc/tutorial/Authentication.lhsnew file+417
@@ -0,0 +1,417 @@
1+# Authentication in Servant
2+
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).
7+
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.
11+
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.
15+
16+## Basic Authentication
17+
18+When protecting endpoints with basic authentication, we need to specify two
19+items:
20+
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.
24+
25+With those two items in mind, *servant* provides the following combinator:
26+
27+``` haskell ignore
28+data BasicAuth (realm :: Symbol) (userData :: *)
29+```
30+
31+You can use this combinator to protect an API as follows:
32+
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 #-}
44+
45+module Authentication where
46+
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,
53+ Get, JSON)
54+import Servant.API.BasicAuth (BasicAuthData (BasicAuthData))
55+import Servant.API.Experimental (AuthProtect)
56+import Servant.Server (BasicAuthCheck (BasicAuthCheck),
57+ BasicAuthResult( Authorized
58+ , Unauthorized
59+ ),
60+ Context ((:.), EmptyContext), Server,
61+ serveWithContext)
62+import Servant.Server.Experimental.Auth (AuthHandler, AuthServerData,
63+ mkAuthHandler)
64+import Servant.Server.Experimenta.Auth()
65+
66+- | private data that needs protection
67+newtype PrivateData = PrivateData { ssshhh :: Text }
68+ deriving (Eq, Show, Generic)
69+
70+instance ToJSON PrivateData
71+
72+-- | public data that anyone can use.
73+newtype PublicData = PublicData { somedata :: Text }
74+ deriving (Eq, Show, Generic)
75+
76+instance ToJSON PublicData
77+
78+-- | A user we'll grab from the database when we authenticate someone
79+newtype User = User { userName :: Text }
80+ deriving (Eq, Show)
81+
82+-- | a type to wrap our public api
83+type PublicAPI = Get '[JSON] [PublicData]
84+
85+-- | a type to wrap our private api
86+type PrivateAPI = Get '[JSON] PrivateData
87+
88+-- | our API
89+type BasicAPI = "public" :> PublicAPI
90+ :<|> "private" :> BasicAuth "foo-realm" User :> PrivateAPI
91+
92+-- | a value holding a proxy of our API type
93+basicAuthApi :: Proxy BasicAPI
94+basicAuthApi = Proxy
95+```
96+
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"`).
101+
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.
107+
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?
119+
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.
127+
128+In practice we wrap `BasicAuthData -> ExceptT ServantErr IO` into a slightly
129+different function to better capture the semantics of basic authentication:
130+
131+``` haskell ignore
132+-- | The result of authentication/authorization
133+data BasicAuthResult usr
134+ = Unauthorized
135+ | BadPassword
136+ | NoSuchUser
137+ | Authorized usr
138+ deriving (Eq, Show, Read, Generic, Typeable, Functor)
139+
140+-- | Datatype wrapping a function used to check authentication.
141+newtype BasicAuthCheck usr = BasicAuthCheck
142+ { unBasicAuthCheck :: BasicAuthData
143+ -> IO (BasicAuthResult usr)
144+ }
145+ deriving (Generic, Typeable, Functor)
146+```
147+
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.
152+
153+```haskell
154+-- | 'BasicAuthCheck' holds the handler we'll use to verify a username and password.
155+authCheck :: BasicAuthCheck User
156+authCheck =
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
162+```
163+
164+And now we create the `Context` used by servant to find `BasicAuthCheck`:
165+
166+```haskell
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
173+```
174+
175+We're now ready to write our `server` method that will tie everything together:
176+
177+```haskell
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
182+server =
183+ let publicAPIHandler = return [PublicData "foo", PublicData "bar"]
184+ privateAPIHandler (user :: User) = return (PrivateData (userName user))
185+ in publicAPIHandler :<|> privateAPIHandler
186+```
187+
188+Finally, our main method and a sample session working with our server:
189+
190+```haskell
191+-- | hello, server!
192+basicAuthMain :: IO ()
193+basicAuthMain = run 8080 (serveWithContext basicAuthApi serverContext server)
194+
195+{- Sample session
196+
197+$ curl -XGET localhost:8080/public
198+[{"somedata":"foo"},{"somedata":"bar"}
199+
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
204+Server: Warp/3.1.8
205+WWW-Authenticate: Basic realm="foo-realm"
206+
207+$ curl -iXGET localhost:8080/private -H "Authorization: Basic c2VydmFudDpzZXJ2ZXI="
208+HTTP/1.1 200 OK
209+transfer-encoding: chunked
210+Date: Thu, 07 Jan 2016 22:37:58 GMT
211+Server: Warp/3.1.8
212+Content-Type: application/json
213+{"ssshhh":"servant"}
214+-}
215+```
216+
217+## Generalized Authentication
218+
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.
223+
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.
230+
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
234+your feedback!
235+
236+### What is Generalized Authentication?
237+
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.
244+
245+### Generalized Authentication in Action
246+
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`.
250+
251+```haskell
252+-- | A user type that we "fetch from the database" after
253+-- performing authentication
254+newtype User = User { unUser :: Text }
255+
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")
261+ ]
262+
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
269+```
270+
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`
274+method:
275+
276+```haskell
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
280+authHandler =
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
285+```
286+
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.
289+
290+```haskell
291+-- | Our API, with auth-protection
292+type AuthGenAPI = "private" :> AuthProtect "cookie-auth" :> PrivateAPI
293+ :<|> "public" :> PublicAPI
294+
295+-- | A value holding our type-level API
296+genAuthApi :: Proxy AuthGenAPI
297+genAuthApi = Proxy
298+```
299+
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
306+fails).
307+
308+```haskell
309+
310+-- | We need to specify the data returned after authentication
311+type instance AuthServerData (AuthProtect "cookie-auth") = User
312+```
313+
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.
317+
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:
320+
321+```haskell
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
327+
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
333+server =
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
338+```
339+
340+We're now ready to start our server (and provide a sample session)!
341+
342+```haskell
343+-- | run our server
344+genAuthMain :: IO ()
345+genAuthMain = run 8080 (serveWithContext api serverContext server)
346+
347+{- Sample Session:
348+
349+$ curl -XGET localhost:8080/private
350+Missing auth header
351+
352+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: key3"
353+[{"ssshhh":"this is a secret: Ghédalia Tazartès"}]
354+
355+$ curl -XGET localhost:8080/private -H "servant-auth-cookie: bad-key"
356+Invalid Cookie
357+
358+$ curl -XGET localhost:8080/public
359+[{"somedata":"this is a public piece of data"}]
360+-}
361+```
362+
363+### Recap
364+
365+Creating a generalized, ad-hoc authentication scheme was fairly straight
366+forward:
367+
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`).
377+
378+Caveats:
379+
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.
384+
385+## Client-side Authentication
386+
387+### Basic Authentication
388+
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.
392+
393+### Generalized Authentication
394+
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,
399+this will look like:
400+
401+```haskell ignore
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
405+
406+-- | A method to authenticate a request
407+authenticateReq :: String -> Req -> Req
408+authenticateReq s req = SCR.addHeader "my-bespoke-header" s req
409+```
410+
411+Now, if the client method for our protected endpoint was `getProtected`, then
412+we could perform authenticated requests as follows:
413+
414+```haskell ignore
415+-- | one could curry this to make it simpler to work with.
416+result = runExceptT (getProtected (mkAuthenticateReq "secret" authenticateReq))
417+```