3These packages provides safe and easy-to-use authentication options for
4`servant`. The same API can be protected via:
11| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12| servant-auth | [](https://hackage.haskell.org/package/servant-auth) |
13| servant-auth-server | [](https://hackage.haskell.org/package/servant-auth-server) |
14| servant-auth-client | [](https://hackage.haskell.org/package/servant-auth-client) |
15| servant-auth-swagger | [](https://hackage.haskell.org/package/servant-auth-swagger) |
16| servant-auth-docs | [](https://hackage.haskell.org/package/servant-auth-docs) |
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)
33import Servant.Auth.Server
34import Servant.Auth.Server.SetCookieOrphan ()
37`servant-auth` library introduces a combinator `Auth`:
40data Auth (auths :: [*]) val
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 :
55Your handlers will get a value of type `AuthResult Something`, and can decide
60data User = User { name :: String, email :: String }
61 deriving (Eq, Show, Read, Generic)
68data Login = Login { username :: String, password :: String }
69 deriving (Eq, Show, Read, Generic)
72instance FromJSON Login
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
89 :> ReqBody '[JSON] Login
90 :> Verb 'POST 204 '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
91 , Header "Set-Cookie" SetCookie]
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
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
111The following example illustrates how to protect an API with tokens.
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.
119 -- We generate the key for signing tokens. This would generally be persisted,
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"
134 xs <- words <$> getLine
136 [name', email'] -> do
137 etoken <- makeJWT (User name' email') jwtCfg Nothing
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"
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)
162 > User-Agent: curl/7.35.0
163 > Host: localhost:7249
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
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)
181 > User-Agent: curl/7.35.0
182 > Host: localhost:7249
184 > Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE
187 < Transfer-Encoding: chunked
188 < Date: Wed, 07 Sep 2016 20:16:11 GMT
189 * Server Warp/3.2.7 is not blacklisted
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
195 * Connection #0 to host localhost left intact
203What if, in addition to API tokens, we want to expose our API to browsers? All
204we need to do is say so!
207mainWithCookies :: IO ()
209 -- We *also* need a key to sign the cookies
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
223 -> Handler (Headers '[ Header "Set-Cookie" SetCookie
224 , Header "Set-Cookie" SetCookie]
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
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:
250var token = (function() {
251 r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))
256$.ajaxPrefilter(function(opts, origOpts, xhr) {
257 xhr.setRequestHeader('X-XSRF-TOKEN', token);
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.
277This README is a literate haskell file. Here is 'main', allowing you to pick
278between the examples above.
285 let usage = "Usage: readme (JWT|Cookie)"
287 ["JWT"] -> mainWithJWT
288 ["Cookie"] -> mainWithCookies
289 e -> putStrLn $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage
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