git.iko.soy/typeable/servant
on the-great-unification help sign in
291 lines 11 kB history raw
1# servant-auth
3These packages provides safe and easy-to-use authentication options for
4`servant`. The same API can be protected via:
5- basicauth
6- cookies
7- JWT tokens
10| Package | Hackage |
11| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12| servant-auth | [![servant-auth](https://img.shields.io/hackage/v/servant-auth?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth) |
13| servant-auth-server | [![servant-auth-server](https://img.shields.io/hackage/v/servant-auth-server.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-server) |
14| servant-auth-client | [![servant-auth-client](https://img.shields.io/hackage/v/servant-auth-client.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-client) |
15| servant-auth-swagger | [![servant-auth-swagger](https://img.shields.io/hackage/v/servant-auth-swagger.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-swagger) |
16| servant-auth-docs | [![servant-auth-docs](https://img.shields.io/hackage/v/servant-auth-docs.svg?style=flat-square&logo=haskell&label&labelColor=5D4F85)](https://hackage.haskell.org/package/servant-auth-docs) |
18## How it works
20First some imports:
22~~~ haskell
23{-# OPTIONS_GHC -fno-warn-unused-binds #-}
24{-# OPTIONS_GHC -fno-warn-deprecations #-}
25import Control.Concurrent (forkIO)
26import Control.Monad (forever)
27import Control.Monad.Trans (liftIO)
28import Data.Aeson (FromJSON, ToJSON)
29import GHC.Generics (Generic)
30import Network.Wai.Handler.Warp (run)
31import System.Environment (getArgs)
32import Servant
33import Servant.Auth.Server
34import Servant.Auth.Server.SetCookieOrphan ()
35~~~
37`servant-auth` library introduces a combinator `Auth`:
39~~~ haskell
40data Auth (auths :: [*]) val
41~~~
43What `Auth [Auth1, Auth2] Something :> API` means is that `API` is protected by
44*either* `Auth1` *or* `Auth2`, and the result of authentication will be of type
45`AuthResult Something`, where :
47~~~ haskell
48data AuthResult val
49 = BadPassword
50 | NoSuchUser
51 | Authenticated val
52 | Indefinite
53~~~
55Your handlers will get a value of type `AuthResult Something`, and can decide
56what to do with it.
58~~~ haskell
60data User = User { name :: String, email :: String }
61 deriving (Eq, Show, Read, Generic)
63instance ToJSON User
64instance ToJWT User
65instance FromJSON User
66instance FromJWT User
68data Login = Login { username :: String, password :: String }
69 deriving (Eq, Show, Read, Generic)
71instance ToJSON Login
72instance FromJSON Login
74type Protected
75 = "name" :> Get '[JSON] String
76 :<|> "email" :> Get '[JSON] String
79-- | 'Protected' will be protected by 'auths', which we still have to specify.
80protected :: Servant.Auth.Server.AuthResult User -> Server Protected
81-- If we get an "Authenticated v", we can trust the information in v, since
82-- it was signed by a key we trust.
83protected (Servant.Auth.Server.Authenticated user) = return (name user) :<|> return (email user)
84-- Otherwise, we return a 401.
85protected _ = throwAll err401
87type Unprotected =
88 "login"
89 :> ReqBody '[JSON] Login
90 :> Verb 'POST 204 '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
91 , Header "Set-Cookie" SetCookie]
92 NoContent)
93 :<|> Raw
95unprotected :: CookieSettings -> JWTSettings -> Server Unprotected
96unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"
98type API auths = (Servant.Auth.Server.Auth auths User :> Protected) :<|> Unprotected
100server :: CookieSettings -> JWTSettings -> Server (API auths)
101server cs jwts = protected :<|> unprotected cs jwts
103~~~
105The code is common to all authentications. In order to pick one or more specific
106authentication methods, all we need to do is provide the expect configuration
107parameters.
109## API tokens
111The following example illustrates how to protect an API with tokens.
114~~~ haskell
115-- In main, we fork the server, and allow new tokens to be created in the
116-- command line for the specified user name and email.
117mainWithJWT :: IO ()
118mainWithJWT = do
119 -- We generate the key for signing tokens. This would generally be persisted,
120 -- and kept safely
121 myKey <- generateKey
122 -- Adding some configurations. All authentications require CookieSettings to
123 -- be in the context.
124 let jwtCfg = defaultJWTSettings myKey
125 cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
126 --- Here we actually make concrete
127 api = Proxy :: Proxy (API '[JWT])
128 _ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
130 putStrLn "Started server on localhost:7249"
131 putStrLn "Enter name and email separated by a space for a new token"
133 forever $ do
134 xs <- words <$> getLine
135 case xs of
136 [name', email'] -> do
137 etoken <- makeJWT (User name' email') jwtCfg Nothing
138 case etoken of
139 Left e -> putStrLn $ "Error generating token:t" ++ show e
140 Right v -> putStrLn $ "New token:\t" ++ show v
141 _ -> putStrLn "Expecting a name and email separated by spaces"
143~~~
145And indeed:
147~~~ bash
149./readme JWT
151 Started server on localhost:7249
152 Enter name and email separated by a space for a new token
153 alice alice@gmail.com
154 New token: "eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"
156curl localhost:7249/name -v
158 * Hostname was NOT found in DNS cache
159 * Trying 127.0.0.1...
160 * Connected to localhost (127.0.0.1) port 7249 (#0)
161 > GET /name HTTP/1.1
162 > User-Agent: curl/7.35.0
163 > Host: localhost:7249
164 > Accept: */*
165 >
166 < HTTP/1.1 401 Unauthorized
167 < Transfer-Encoding: chunked
168 < Date: Wed, 07 Sep 2016 20:17:17 GMT
169 * Server Warp/3.2.7 is not blacklisted
170 < Server: Warp/3.2.7
171 <
172 * Connection #0 to host localhost left intact
174curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \
175 localhost:7249/name -v
177 * Hostname was NOT found in DNS cache
178 * Trying 127.0.0.1...
179 * Connected to localhost (127.0.0.1) port 7249 (#0)
180 > GET /name HTTP/1.1
181 > User-Agent: curl/7.35.0
182 > Host: localhost:7249
183 > Accept: */*
184 > Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE
185 >
186 < HTTP/1.1 200 OK
187 < Transfer-Encoding: chunked
188 < Date: Wed, 07 Sep 2016 20:16:11 GMT
189 * Server Warp/3.2.7 is not blacklisted
190 < Server: Warp/3.2.7
191 < Content-Type: application/json
192 < Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure
193 < Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure
194 <
195 * Connection #0 to host localhost left intact
196 "alice"%
199~~~
201## Cookies
203What if, in addition to API tokens, we want to expose our API to browsers? All
204we need to do is say so!
206~~~ haskell
207mainWithCookies :: IO ()
208mainWithCookies = do
209 -- We *also* need a key to sign the cookies
210 myKey <- generateKey
211 -- Adding some configurations. 'Cookie' requires, in addition to
212 -- CookieSettings, JWTSettings (for signing), so everything is just as before
213 let jwtCfg = defaultJWTSettings myKey
214 cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
215 --- Here is the actual change
216 api = Proxy :: Proxy (API '[Cookie])
217 run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
219-- Here is the login handler
220checkCreds :: CookieSettings
221 -> JWTSettings
222 -> Login
223 -> Handler (Headers '[ Header "Set-Cookie" SetCookie
224 , Header "Set-Cookie" SetCookie]
225 NoContent)
226checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
227 -- Usually you would ask a database for the user info. This is just a
228 -- regular servant handler, so you can follow your normal database access
229 -- patterns (including using 'enter').
230 let usr = User "Ali Baba" "ali@email.com"
231 mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr
232 case mApplyCookies of
233 Nothing -> throwError err401
234 Just applyCookies -> return $ applyCookies NoContent
235checkCreds _ _ _ = throwError err401
236~~~
238### XSRF and the frontend
240XSRF protection works by requiring that there be a header of the same value as
241a distinguished cookie that is set by the server on each request. What the
242cookie and header name are can be configured (see `xsrfCookieName` and
243`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and
244"X-XSRF-TOKEN". This means that, if your client is a browser and you're using
245cookies, Javascript on the client must set the header of each request by
246reading the cookie. For jQuery, and with the default values, that might be:
248~~~ javascript
250var token = (function() {
251 r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))
252 if (r) return r[1];
253})();
256$.ajaxPrefilter(function(opts, origOpts, xhr) {
257 xhr.setRequestHeader('X-XSRF-TOKEN', token);
258 }
260~~~
262I *believe* nothing at all needs to be done if you're using Angular's `$http`
263directive, but I haven't tested this.
265XSRF protection can be disabled just for `GET` requests by setting
266`xsrfExcludeGet = False`. You might want this if you're relying on the browser
267to navigate between pages that require cookie authentication.
269XSRF protection can be completely disabled by setting `cookieXsrfSetting =
270Nothing` in `CookieSettings`. This is not recommended! If your cookie
271authenticated web application runs any javascript, it's recommended to send the
272XSRF header. However, if your web application runs no javascript, disabling
273XSRF entirely may be required.
275# Note on this README
277This README is a literate haskell file. Here is 'main', allowing you to pick
278between the examples above.
280~~~ haskell
282main :: IO ()
283main = do
284 args <- getArgs
285 let usage = "Usage: readme (JWT|Cookie)"
286 case args of
287 ["JWT"] -> mainWithJWT
288 ["Cookie"] -> mainWithCookies
289 e -> putStrLn $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage
291~~~