packages feed

servant-auth-server 0.3.2.0 → 0.4.0.0

raw patch · 10 files changed

+633/−407 lines, 10 filesdep +semigroupsdep +servantdep ~basedep ~cookiedep ~http-api-datanew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: semigroups, servant

Dependency ranges changed: base, cookie, http-api-data, jose, monad-time, servant-server

API changes (from Hackage documentation)

- Servant.Auth.Server: [key] :: JWTSettings -> JWK
- Servant.Auth.Server.Internal.ConfigTypes: [key] :: JWTSettings -> JWK
- Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Classes.Eq Servant.Auth.Server.Internal.ConfigTypes.IsSecure
- Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Classes.Ord Servant.Auth.Server.Internal.ConfigTypes.IsSecure
- Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Generics.Generic Servant.Auth.Server.Internal.ConfigTypes.IsSecure
- Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Read.Read Servant.Auth.Server.Internal.ConfigTypes.IsSecure
- Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Show.Show Servant.Auth.Server.Internal.ConfigTypes.IsSecure
+ Servant.Auth.Server: AnySite :: SameSite
+ Servant.Auth.Server: SameSiteLax :: SameSite
+ Servant.Auth.Server: SameSiteStrict :: SameSite
+ Servant.Auth.Server: XsrfCookieSettings :: !ByteString -> !(Maybe ByteString) -> !ByteString -> !Bool -> XsrfCookieSettings
+ Servant.Auth.Server: [cookieXsrfSetting] :: CookieSettings -> !(Maybe XsrfCookieSettings)
+ Servant.Auth.Server: [jwtAlg] :: JWTSettings -> Maybe Alg
+ Servant.Auth.Server: [signingKey] :: JWTSettings -> JWK
+ Servant.Auth.Server: [validationKeys] :: JWTSettings -> JWKSet
+ Servant.Auth.Server: clearSession :: (AddHeader "Set-Cookie" SetCookie response withOneCookie, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies) => CookieSettings -> response -> withTwoCookies
+ Servant.Auth.Server: data SameSite
+ Servant.Auth.Server: data XsrfCookieSettings
+ Servant.Auth.Server: defaultXsrfCookieSettings :: XsrfCookieSettings
+ Servant.Auth.Server: makeXsrfCookie :: CookieSettings -> IO SetCookie
+ Servant.Auth.Server.Internal.ConfigTypes: XsrfCookieSettings :: !ByteString -> !(Maybe ByteString) -> !ByteString -> !Bool -> XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: [cookieXsrfSetting] :: CookieSettings -> !(Maybe XsrfCookieSettings)
+ Servant.Auth.Server.Internal.ConfigTypes: [jwtAlg] :: JWTSettings -> Maybe Alg
+ Servant.Auth.Server.Internal.ConfigTypes: [signingKey] :: JWTSettings -> JWK
+ Servant.Auth.Server.Internal.ConfigTypes: [validationKeys] :: JWTSettings -> JWKSet
+ Servant.Auth.Server.Internal.ConfigTypes: data XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: defaultXsrfCookieSettings :: XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: instance Data.Default.Class.Default Servant.Auth.Server.Internal.ConfigTypes.XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Classes.Eq Servant.Auth.Server.Internal.ConfigTypes.XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Generics.Generic Servant.Auth.Server.Internal.ConfigTypes.XsrfCookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: instance GHC.Show.Show Servant.Auth.Server.Internal.ConfigTypes.XsrfCookieSettings
+ Servant.Auth.Server.Internal.Cookie: applyCookieSettings :: CookieSettings -> SetCookie -> SetCookie
+ Servant.Auth.Server.Internal.Cookie: applySessionCookieSettings :: CookieSettings -> SetCookie -> SetCookie
+ Servant.Auth.Server.Internal.Cookie: applyXsrfCookieSettings :: XsrfCookieSettings -> SetCookie -> SetCookie
+ Servant.Auth.Server.Internal.Cookie: clearSession :: (AddHeader "Set-Cookie" SetCookie response withOneCookie, AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies) => CookieSettings -> response -> withTwoCookies
+ Servant.Auth.Server.Internal.Cookie: makeXsrfCookie :: CookieSettings -> IO SetCookie
+ Servant.Auth.Server.Internal.Cookie: noXsrfTokenCookie :: CookieSettings -> SetCookie
+ Servant.Auth.Server.Internal.Cookie: xsrfCheckRequired :: CookieSettings -> Request -> Maybe XsrfCookieSettings
+ Servant.Auth.Server.Internal.Cookie: xsrfCookieAuthCheck :: XsrfCookieSettings -> Request -> [(ByteString, ByteString)] -> Bool
+ Servant.Auth.Server.Internal.Types: instance Data.Semigroup.Semigroup (Servant.Auth.Server.Internal.Types.AuthCheck val)
+ Servant.Auth.Server.Internal.Types: instance Data.Semigroup.Semigroup (Servant.Auth.Server.Internal.Types.AuthResult val)
- Servant.Auth.Server: BasicAuthData :: ~ByteString -> ~ByteString -> BasicAuthData
+ Servant.Auth.Server: BasicAuthData :: !ByteString -> !ByteString -> BasicAuthData
- Servant.Auth.Server: CookieSettings :: IsSecure -> Maybe DiffTime -> Maybe UTCTime -> Maybe ByteString -> SameSite -> ByteString -> ByteString -> Maybe ByteString -> ByteString -> Bool -> CookieSettings
+ Servant.Auth.Server: CookieSettings :: !IsSecure -> !(Maybe DiffTime) -> !(Maybe UTCTime) -> !(Maybe ByteString) -> !SameSite -> !ByteString -> !(Maybe XsrfCookieSettings) -> CookieSettings
- Servant.Auth.Server: JWTSettings :: JWK -> (StringOrURI -> IsMatch) -> JWTSettings
+ Servant.Auth.Server: JWTSettings :: JWK -> Maybe Alg -> JWKSet -> (StringOrURI -> IsMatch) -> JWTSettings
- Servant.Auth.Server: [basicAuthPassword] :: BasicAuthData -> ~ByteString
+ Servant.Auth.Server: [basicAuthPassword] :: BasicAuthData -> !ByteString
- Servant.Auth.Server: [basicAuthUsername] :: BasicAuthData -> ~ByteString
+ Servant.Auth.Server: [basicAuthUsername] :: BasicAuthData -> !ByteString
- Servant.Auth.Server: [cookieExpires] :: CookieSettings -> Maybe UTCTime
+ Servant.Auth.Server: [cookieExpires] :: CookieSettings -> !(Maybe UTCTime)
- Servant.Auth.Server: [cookieIsSecure] :: CookieSettings -> IsSecure
+ Servant.Auth.Server: [cookieIsSecure] :: CookieSettings -> !IsSecure
- Servant.Auth.Server: [cookieMaxAge] :: CookieSettings -> Maybe DiffTime
+ Servant.Auth.Server: [cookieMaxAge] :: CookieSettings -> !(Maybe DiffTime)
- Servant.Auth.Server: [cookiePath] :: CookieSettings -> Maybe ByteString
+ Servant.Auth.Server: [cookiePath] :: CookieSettings -> !(Maybe ByteString)
- Servant.Auth.Server: [cookieSameSite] :: CookieSettings -> SameSite
+ Servant.Auth.Server: [cookieSameSite] :: CookieSettings -> !SameSite
- Servant.Auth.Server: [sessionCookieName] :: CookieSettings -> ByteString
+ Servant.Auth.Server: [sessionCookieName] :: CookieSettings -> !ByteString
- Servant.Auth.Server: [xsrfCookieName] :: CookieSettings -> ByteString
+ Servant.Auth.Server: [xsrfCookieName] :: XsrfCookieSettings -> !ByteString
- Servant.Auth.Server: [xsrfCookiePath] :: CookieSettings -> Maybe ByteString
+ Servant.Auth.Server: [xsrfCookiePath] :: XsrfCookieSettings -> !(Maybe ByteString)
- Servant.Auth.Server: [xsrfExcludeGet] :: CookieSettings -> Bool
+ Servant.Auth.Server: [xsrfExcludeGet] :: XsrfCookieSettings -> !Bool
- Servant.Auth.Server: [xsrfHeaderName] :: CookieSettings -> ByteString
+ Servant.Auth.Server: [xsrfHeaderName] :: XsrfCookieSettings -> !ByteString
- Servant.Auth.Server: class FromJWT a where decodeJWT m = case lookup "dat" (m ^. unregisteredClaims) of { Nothing -> Left "Missing 'dat' claim" Just v -> case fromJSON v of { Error e -> Left $ pack e Success a -> Right a } }
+ Servant.Auth.Server: class FromJWT a
- Servant.Auth.Server: class ToJWT a where encodeJWT a = addClaim "dat" (toJSON a) emptyClaimsSet
+ Servant.Auth.Server: class ToJWT a
- Servant.Auth.Server: data IsSecure
+ Servant.Auth.Server: data IsSecure :: *
- Servant.Auth.Server.Internal.AddSetCookie: [SetCookieCons] :: Maybe SetCookie -> SetCookieList n -> SetCookieList (S n)
+ Servant.Auth.Server.Internal.AddSetCookie: [SetCookieCons] :: Maybe SetCookie -> SetCookieList n -> SetCookieList ( 'S n)
- Servant.Auth.Server.Internal.AddSetCookie: [SetCookieNil] :: SetCookieList Z
+ Servant.Auth.Server.Internal.AddSetCookie: [SetCookieNil] :: SetCookieList 'Z
- Servant.Auth.Server.Internal.Class: class IsAuth a v where type AuthArgs a :: [*] where {
+ Servant.Auth.Server.Internal.Class: class IsAuth a v where {
- Servant.Auth.Server.Internal.ConfigTypes: CookieSettings :: IsSecure -> Maybe DiffTime -> Maybe UTCTime -> Maybe ByteString -> SameSite -> ByteString -> ByteString -> Maybe ByteString -> ByteString -> Bool -> CookieSettings
+ Servant.Auth.Server.Internal.ConfigTypes: CookieSettings :: !IsSecure -> !(Maybe DiffTime) -> !(Maybe UTCTime) -> !(Maybe ByteString) -> !SameSite -> !ByteString -> !(Maybe XsrfCookieSettings) -> CookieSettings
- Servant.Auth.Server.Internal.ConfigTypes: JWTSettings :: JWK -> (StringOrURI -> IsMatch) -> JWTSettings
+ Servant.Auth.Server.Internal.ConfigTypes: JWTSettings :: JWK -> Maybe Alg -> JWKSet -> (StringOrURI -> IsMatch) -> JWTSettings
- Servant.Auth.Server.Internal.ConfigTypes: [cookieExpires] :: CookieSettings -> Maybe UTCTime
+ Servant.Auth.Server.Internal.ConfigTypes: [cookieExpires] :: CookieSettings -> !(Maybe UTCTime)
- Servant.Auth.Server.Internal.ConfigTypes: [cookieIsSecure] :: CookieSettings -> IsSecure
+ Servant.Auth.Server.Internal.ConfigTypes: [cookieIsSecure] :: CookieSettings -> !IsSecure
- Servant.Auth.Server.Internal.ConfigTypes: [cookieMaxAge] :: CookieSettings -> Maybe DiffTime
+ Servant.Auth.Server.Internal.ConfigTypes: [cookieMaxAge] :: CookieSettings -> !(Maybe DiffTime)
- Servant.Auth.Server.Internal.ConfigTypes: [cookiePath] :: CookieSettings -> Maybe ByteString
+ Servant.Auth.Server.Internal.ConfigTypes: [cookiePath] :: CookieSettings -> !(Maybe ByteString)
- Servant.Auth.Server.Internal.ConfigTypes: [cookieSameSite] :: CookieSettings -> SameSite
+ Servant.Auth.Server.Internal.ConfigTypes: [cookieSameSite] :: CookieSettings -> !SameSite
- Servant.Auth.Server.Internal.ConfigTypes: [sessionCookieName] :: CookieSettings -> ByteString
+ Servant.Auth.Server.Internal.ConfigTypes: [sessionCookieName] :: CookieSettings -> !ByteString
- Servant.Auth.Server.Internal.ConfigTypes: [xsrfCookieName] :: CookieSettings -> ByteString
+ Servant.Auth.Server.Internal.ConfigTypes: [xsrfCookieName] :: XsrfCookieSettings -> !ByteString
- Servant.Auth.Server.Internal.ConfigTypes: [xsrfCookiePath] :: CookieSettings -> Maybe ByteString
+ Servant.Auth.Server.Internal.ConfigTypes: [xsrfCookiePath] :: XsrfCookieSettings -> !(Maybe ByteString)
- Servant.Auth.Server.Internal.ConfigTypes: [xsrfExcludeGet] :: CookieSettings -> Bool
+ Servant.Auth.Server.Internal.ConfigTypes: [xsrfExcludeGet] :: XsrfCookieSettings -> !Bool
- Servant.Auth.Server.Internal.ConfigTypes: [xsrfHeaderName] :: CookieSettings -> ByteString
+ Servant.Auth.Server.Internal.ConfigTypes: [xsrfHeaderName] :: XsrfCookieSettings -> !ByteString
- Servant.Auth.Server.Internal.ConfigTypes: data IsSecure
+ Servant.Auth.Server.Internal.ConfigTypes: data IsSecure :: *
- Servant.Auth.Server.Internal.JWT: class FromJWT a where decodeJWT m = case lookup "dat" (m ^. unregisteredClaims) of { Nothing -> Left "Missing 'dat' claim" Just v -> case fromJSON v of { Error e -> Left $ pack e Success a -> Right a } }
+ Servant.Auth.Server.Internal.JWT: class FromJWT a
- Servant.Auth.Server.Internal.JWT: class ToJWT a where encodeJWT a = addClaim "dat" (toJSON a) emptyClaimsSet
+ Servant.Auth.Server.Internal.JWT: class ToJWT a

Files

+ README.lhs view
@@ -0,0 +1,284 @@+# servant-auth++[![Build Status](https://travis-ci.org/haskell-servant/servant-auth.svg?branch=master)](https://travis-ci.org/haskell-servant/servant-auth)++This package provides safe and easy-to-use authentication options for+`servant`. The same API can be protected via login and cookies, or API tokens,+without much extra work.+++## How it works++First some imports:++~~~ haskell+{-# OPTIONS_GHC -fno-warn-unused-binds #-}+{-# OPTIONS_GHC -fno-warn-deprecations #-}+import Control.Concurrent (forkIO)+import Control.Monad (forever)+import Control.Monad.Trans (liftIO)+import Data.Aeson (FromJSON, ToJSON)+import GHC.Generics (Generic)+import Network.Wai.Handler.Warp (run)+import System.Environment (getArgs)+import Servant+import Servant.Auth.Server+import Servant.Auth.Server.SetCookieOrphan ()+~~~++`servant-auth` library introduces a combinator `Auth`:++~~~ haskell+data Auth (auths :: [*]) val+~~~++What `Auth [Auth1, Auth2] Something :> API` means is that `API` is protected by+*either* `Auth1` *or* `Auth2`, and the result of authentication will be of type+`AuthResult Something`, where :++~~~ haskell+data AuthResult val+  = BadPassword+  | NoSuchUser+  | Authenticated val+  | Indefinite+~~~++Your handlers will get a value of type `AuthResult Something`, and can decide+what to do with it.++~~~ haskell++data User = User { name :: String, email :: String }+   deriving (Eq, Show, Read, Generic)++instance ToJSON User+instance ToJWT User+instance FromJSON User+instance FromJWT User++data Login = Login { username :: String, password :: String }+   deriving (Eq, Show, Read, Generic)++instance ToJSON Login+instance FromJSON Login++type Protected+   = "name" :> Get '[JSON] String+ :<|> "email" :> Get '[JSON] String+++-- | 'Protected' will be protected by 'auths', which we still have to specify.+protected :: Servant.Auth.Server.AuthResult User -> Server Protected+-- If we get an "Authenticated v", we can trust the information in v, since+-- it was signed by a key we trust.+protected (Servant.Auth.Server.Authenticated user) = return (name user) :<|> return (email user)+-- Otherwise, we return a 401.+protected _ = throwAll err401++type Unprotected =+ "login"+     :> ReqBody '[JSON] Login+     :> PostNoContent '[JSON] (Headers '[ Header "Set-Cookie" SetCookie+                                        , Header "Set-Cookie" SetCookie]+                                       NoContent)+  :<|> Raw++unprotected :: CookieSettings -> JWTSettings -> Server Unprotected+unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"++type API auths = (Servant.Auth.Server.Auth auths User :> Protected) :<|> Unprotected++server :: CookieSettings -> JWTSettings -> Server (API auths)+server cs jwts = protected :<|> unprotected cs jwts++~~~++The code is common to all authentications. In order to pick one or more specific+authentication methods, all we need to do is provide the expect configuration+parameters.++## API tokens++The following example illustrates how to protect an API with tokens.+++~~~ haskell+-- In main, we fork the server, and allow new tokens to be created in the+-- command line for the specified user name and email.+mainWithJWT :: IO ()+mainWithJWT = do+  -- We generate the key for signing tokens. This would generally be persisted,+  -- and kept safely+  myKey <- generateKey+  -- Adding some configurations. All authentications require CookieSettings to+  -- be in the context.+  let jwtCfg = defaultJWTSettings myKey+      cfg = defaultCookieSettings :. jwtCfg :. EmptyContext+      --- Here we actually make concrete+      api = Proxy :: Proxy (API '[JWT])+  _ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)++  putStrLn "Started server on localhost:7249"+  putStrLn "Enter name and email separated by a space for a new token"++  forever $ do+     xs <- words <$> getLine+     case xs of+       [name', email'] -> do+         etoken <- makeJWT (User name' email') jwtCfg Nothing+         case etoken of+           Left e -> putStrLn $ "Error generating token:t" ++ show e+           Right v -> putStrLn $ "New token:\t" ++ show v+       _ -> putStrLn "Expecting a name and email separated by spaces"++~~~++And indeed:++~~~ bash++./readme JWT++    Started server on localhost:7249+    Enter name and email separated by a space for a new token+    alice alice@gmail.com+    New token:	"eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"++curl localhost:7249/name -v++    * Hostname was NOT found in DNS cache+    *   Trying 127.0.0.1...+    * Connected to localhost (127.0.0.1) port 7249 (#0)+    > GET /name HTTP/1.1+    > User-Agent: curl/7.35.0+    > Host: localhost:7249+    > Accept: */*+    >+    < HTTP/1.1 401 Unauthorized+    < Transfer-Encoding: chunked+    < Date: Wed, 07 Sep 2016 20:17:17 GMT+    * Server Warp/3.2.7 is not blacklisted+    < Server: Warp/3.2.7+    <+    * Connection #0 to host localhost left intact++curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \+  localhost:7249/name -v++    * Hostname was NOT found in DNS cache+    *   Trying 127.0.0.1...+    * Connected to localhost (127.0.0.1) port 7249 (#0)+    > GET /name HTTP/1.1+    > User-Agent: curl/7.35.0+    > Host: localhost:7249+    > Accept: */*+    > Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE+    >+    < HTTP/1.1 200 OK+    < Transfer-Encoding: chunked+    < Date: Wed, 07 Sep 2016 20:16:11 GMT+    * Server Warp/3.2.7 is not blacklisted+    < Server: Warp/3.2.7+    < Content-Type: application/json+    < Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure+    <  Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure+    <+    * Connection #0 to host localhost left intact+    "alice"%+++~~~++## Cookies++What if, in addition to API tokens, we want to expose our API to browsers? All+we need to do is say so!++~~~ haskell+mainWithCookies :: IO ()+mainWithCookies = do+  -- We *also* need a key to sign the cookies+  myKey <- generateKey+  -- Adding some configurations. 'Cookie' requires, in addition to+  -- CookieSettings, JWTSettings (for signing), so everything is just as before+  let jwtCfg = defaultJWTSettings myKey+      cfg = defaultCookieSettings :. jwtCfg :. EmptyContext+      --- Here is the actual change+      api = Proxy :: Proxy (API '[Cookie])+  run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)+++-- Here is the login handler+checkCreds :: CookieSettings+           -> JWTSettings+           -> Login+           -> Handler (Headers '[ Header "Set-Cookie" SetCookie+                                , Header "Set-Cookie" SetCookie]+                               NoContent)+checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do+   -- Usually you would ask a database for the user info. This is just a+   -- regular servant handler, so you can follow your normal database access+   -- patterns (including using 'enter').+   let usr = User "Ali Baba" "ali@email.com"+   mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr+   case mApplyCookies of+     Nothing           -> throwError err401+     Just applyCookies -> return $ applyCookies NoContent+checkCreds _ _ _ = throwError err401+~~~++### XSRF and the frontend++XSRF protection works by requiring that there be a header of the same value as+a distinguished cookie that is set by the server on each request. What the+cookie and header name are can be configured (see `xsrfCookieName` and+`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and+"X-XSRF-TOKEN". This means that, if your client is a browser and your are using+cookies, Javascript on the client must set the header of each request by+reading the cookie. For jQuery, and with the default values, that might be:++~~~ javascript++var token = (function() {+  r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))+  if (r) return r[1];+)();+++$.ajaxPrefilter(function(opts, origOpts, xhr) {+  xhr.setRequestHeader('X-XSRF-TOKEN', token);+  }++~~~++I *believe* nothing at all needs to be done if you're using Angular's `$http`+directive, but I haven't tested this.++XSRF protection can be disabled just for `GET` requests by setting+`xsrfExcludeGet = False`. You might want this if you're relying on the browser+to navigate between pages that require cookie authentication.++XSRF protection can be completely disabled by setting `cookieXsrfSetting =+Nothing` in `CookieSettings`. This is not recommended! If your cookie+authenticated web application runs any javascript, it's recommended to send the+XSRF header. However, if your web application runs no javascript, disabling+XSRF entirely may be required.++# Note on this README++This README is a literate haskell file. Here is 'main', allowing you to pick+between the examples above.++~~~ haskell++main :: IO ()+main = do+  args <- getArgs+  let usage = "Usage: readme (JWT|Cookie)"+  case args of+    ["JWT"] -> mainWithJWT+    ["Cookie"] -> mainWithCookies+    e -> putStrLn $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage++~~~
− executables/README.lhs
@@ -1,270 +0,0 @@-# servant-auth--[![Build Status](https://travis-ci.org/haskell-servant/servant-auth.svg?branch=master)](https://travis-ci.org/haskell-servant/servant-auth)--This package provides safe and easy-to-use authentication options for-`servant`. The same API can be protected via login and cookies, or API tokens,-without much extra work.---## How it works--This library introduces a combinator `Auth`:--~~~ {.haskell ignore}-Auth (auths :: [*]) val-~~~--What `Auth [Auth1, Auth2] Something :> API` means is that `API` is protected by-*either* `Auth1` *or* `Auth2`, and the result of authentication will be of type-`AuthResult Something`, where :--~~~ {.haskell ignore}-data AuthResult val-  = BadPassword-  | NoSuchUser-  | Authenticated val-  | Indefinite-~~~--Your handlers will get a value of type `AuthResult Something`, and can decide-what to do with it.--~~~ {.haskell}-{-# OPTIONS_GHC -fno-warn-unused-binds #-}-{-# OPTIONS_GHC -fno-warn-deprecations #-}-import Control.Concurrent (forkIO)-import Control.Monad (forever)-import Control.Monad.Trans (liftIO)-import Data.Aeson (FromJSON, ToJSON)-import GHC.Generics (Generic)-import Network.Wai.Handler.Warp (run)-import System.Environment (getArgs)-import Servant-import Servant.Auth.Server-import Servant.Auth.Server.SetCookieOrphan ()--data User = User { name :: String, email :: String }-   deriving (Eq, Show, Read, Generic)--instance ToJSON User-instance ToJWT User-instance FromJSON User-instance FromJWT User--data Login = Login { username :: String, password :: String }-   deriving (Eq, Show, Read, Generic)--instance ToJSON Login-instance FromJSON Login--type Protected-   = "name" :> Get '[JSON] String- :<|> "email" :> Get '[JSON] String----- | 'Protected' will be protected by 'auths', which we still have to specify.-protected :: AuthResult User -> Server Protected--- If we get an "Authenticated v", we can trust the information in v, since--- it was signed by a key we trust.-protected (Authenticated user) = return (name user) :<|> return (email user)--- Otherwise, we return a 401.-protected _ = throwAll err401--type Unprotected =- "login"-     :> ReqBody '[JSON] Login-     :> PostNoContent '[JSON] (Headers '[ Header "Set-Cookie" SetCookie-                                        , Header "Set-Cookie" SetCookie]-                                       NoContent)-  :<|> Raw--unprotected :: CookieSettings -> JWTSettings -> Server Unprotected-unprotected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"--type API auths = (Auth auths User :> Protected) :<|> Unprotected--server :: CookieSettings -> JWTSettings -> Server (API auths)-server cs jwts = protected :<|> unprotected cs jwts--~~~--The code is common to all authentications. In order to pick one or more specific-authentication methods, all we need to do is provide the expect configuration-parameters.--## API tokens--The following example illustrates how to protect an API with tokens.---~~~ {.haskell}--- In main, we fork the server, and allow new tokens to be created in the--- command line for the specified user name and email.-mainWithJWT :: IO ()-mainWithJWT = do-  -- We generate the key for signing tokens. This would generally be persisted,-  -- and kept safely-  myKey <- generateKey-  -- Adding some configurations. All authentications require CookieSettings to-  -- be in the context.-  let jwtCfg = defaultJWTSettings myKey-      cfg = defaultCookieSettings :. jwtCfg :. EmptyContext-      --- Here we actually make concrete-      api = Proxy :: Proxy (API '[JWT])-  _ <- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)--  putStrLn "Started server on localhost:7249"-  putStrLn "Enter name and email separated by a space for a new token"--  forever $ do-     xs <- words <$> getLine-     case xs of-       [name', email'] -> do-         etoken <- makeJWT (User name' email') jwtCfg Nothing-         case etoken of-           Left e -> putStrLn $ "Error generating token:t" ++ show e-           Right v -> putStrLn $ "New token:\t" ++ show v-       _ -> putStrLn "Expecting a name and email separated by spaces"--~~~--And indeed:--~~~ {.bash}--./readme JWT--    Started server on localhost:7249-    Enter name and email separated by a space for a new token-    alice alice@gmail.com-    New token:	"eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"--curl localhost:7249/name -v--    * Hostname was NOT found in DNS cache-    *   Trying 127.0.0.1...-    * Connected to localhost (127.0.0.1) port 7249 (#0)-    > GET /name HTTP/1.1-    > User-Agent: curl/7.35.0-    > Host: localhost:7249-    > Accept: */*-    >-    < HTTP/1.1 401 Unauthorized-    < Transfer-Encoding: chunked-    < Date: Wed, 07 Sep 2016 20:17:17 GMT-    * Server Warp/3.2.7 is not blacklisted-    < Server: Warp/3.2.7-    <-    * Connection #0 to host localhost left intact--curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \-  localhost:7249/name -v--    * Hostname was NOT found in DNS cache-    *   Trying 127.0.0.1...-    * Connected to localhost (127.0.0.1) port 7249 (#0)-    > GET /name HTTP/1.1-    > User-Agent: curl/7.35.0-    > Host: localhost:7249-    > Accept: */*-    > Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE-    >-    < HTTP/1.1 200 OK-    < Transfer-Encoding: chunked-    < Date: Wed, 07 Sep 2016 20:16:11 GMT-    * Server Warp/3.2.7 is not blacklisted-    < Server: Warp/3.2.7-    < Content-Type: application/json-    < Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure-    <  Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure-    <-    * Connection #0 to host localhost left intact-    "alice"%---~~~--## Cookies--What if, in addition to API tokens, we want to expose our API to browsers? All-we need to do is say so!--~~~ {.haskell}-mainWithCookies :: IO ()-mainWithCookies = do-  -- We *also* need a key to sign the cookies-  myKey <- generateKey-  -- Adding some configurations. 'Cookie' requires, in addition to-  -- CookieSettings, JWTSettings (for signing), so everything is just as before-  let jwtCfg = defaultJWTSettings myKey-      cfg = defaultCookieSettings :. jwtCfg :. EmptyContext-      --- Here is the actual change-      api = Proxy :: Proxy (API '[Cookie])-  run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)----- Here is the login handler-checkCreds :: CookieSettings-           -> JWTSettings-           -> Login-           -> Handler (Headers '[ Header "Set-Cookie" SetCookie-                                , Header "Set-Cookie" SetCookie]-                               NoContent)-checkCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do-   -- Usually you would ask a database for the user info. This is just a-   -- regular servant handler, so you can follow your normal database access-   -- patterns (including using 'enter').-   let usr = User "Ali Baba" "ali@email.com"-   mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr-   case mApplyCookies of-     Nothing           -> throwError err401-     Just applyCookies -> return $ applyCookies NoContent-checkCreds _ _ _ = throwError err401-~~~--### CSRF and the frontend--CSRF protection works by requiring that there be a header of the same value as-a distinguished cookie that is set by the server on each request. What the-cookie and header name are can be configured (see `xsrfCookieName` and-`xsrfHeaderName` in `CookieSettings`), but by default they are "XSRF-TOKEN" and-"X-XSRF-TOKEN". This means that, if your client is a browser and your are using-cookies, Javascript on the client must set the header of each request by-reading the cookie. For jQuery, and with the default values, that might be:--~~~ { .javascript }--var token = (function() {-  r = document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))-  if (r) return r[1];-)();---$.ajaxPrefilter(function(opts, origOpts, xhr) {-  xhr.setRequestHeader('X-XSRF-TOKEN', token);-  }--~~~---I *believe* nothing at all needs to be done if you're using Angular's `$http`-directive, but I haven't tested this.--# Note on this README--This README is a literate haskell file. Here is 'main', allowing you to pick-between the examples above.--~~~ { .haskell }--main :: IO ()-main = do-  args <- getArgs-  let usage = "Usage: readme (JWT|Cookie)"-  case args of-    ["JWT"] -> mainWithJWT-    ["Cookie"] -> mainWithCookies-    e -> error $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage--~~~
servant-auth-server.cabal view
@@ -1,5 +1,5 @@ name:           servant-auth-server-version:        0.3.2.0+version:        0.4.0.0 synopsis:       servant-server/servant-auth compatibility description:    This package provides the required instances for using the @Auth@ combinator                 in your 'servant' server.@@ -15,7 +15,7 @@ copyright:      (c) Julian K. Arni license:        BSD3 license-file:   LICENSE-tested-with:    GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2+tested-with:    GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.3 build-type:     Simple cabal-version:  >= 1.10 @@ -29,30 +29,34 @@   default-extensions: AutoDeriveTypeable ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators   ghc-options: -Wall   build-depends:-      base                    >= 4.8  && < 4.11+      base                    >= 4.8  && < 4.12     , aeson                   >= 0.11 && < 2     , base64-bytestring       >= 1    && < 2     , blaze-builder           >= 0.4  && < 0.5     , bytestring              >= 0.10 && < 0.11     , bytestring-conversion   >= 0.3  && < 0.4     , case-insensitive        >= 1.2  && < 1.3-    , cookie                  >= 0.4  && < 0.4.4+    , cookie                  >= 0.4  && < 0.5     , crypto-api              >= 0.13 && < 0.14     , data-default-class      >= 0.0  && < 0.2     , entropy                 >= 0.3  && < 0.5-    , http-api-data           >= 0.3  && < 0.3.8+    , http-api-data           >= 0.3.7 && < 0.4     , http-types              >= 0.9  && < 0.13-    , jose                    >= 0.6  && < 0.7+    , jose                    >= 0.6  && < 0.8     , lens                    >= 4    && < 5-    , monad-time              >= 0.2  && < 0.3+    , monad-time              >= 0.2  && < 0.4     , mtl                     >= 2.2  && < 2.3+    , servant                 >= 0.9.1  && < 0.15     , servant-auth            == 0.3.*-    , servant-server          >= 0.9.1  && < 0.14+    , servant-server          >= 0.9.1  && < 0.15     , tagged                  >= 0.7.3 && < 0.9     , text                    >= 1    && < 2     , time                    >= 1.5  && < 1.9     , unordered-containers    >= 0.2  && < 0.3     , wai                     >= 3.2  && < 3.3+  if !impl(ghc >= 8.0)+    build-depends:+      semigroups >= 0.18.1 && <0.19   exposed-modules:       Servant.Auth.Server       Servant.Auth.Server.Internal@@ -68,10 +72,9 @@       Servant.Auth.Server.SetCookieOrphan   default-language: Haskell2010 -executable readme+test-suite readme+  type: exitcode-stdio-1.0   main-is: README.lhs-  hs-source-dirs:-      executables   default-extensions: AutoDeriveTypeable ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveTraversable FlexibleContexts FlexibleInstances FunctionalDependencies GADTs KindSignatures MultiParamTypeClasses OverloadedStrings RankNTypes ScopedTypeVariables TypeFamilies TypeOperators   ghc-options: -Wall -pgmL markdown-unlit   build-tool-depends: markdown-unlit:markdown-unlit@@ -111,6 +114,7 @@     , http-types     , wai     , servant-server+    , transformers    -- test dependencies   build-depends:
src/Servant/Auth/Server.hs view
@@ -67,18 +67,22 @@    -- ** Settings   , CookieSettings(..)+  , XsrfCookieSettings(..)   , defaultCookieSettings+  , defaultXsrfCookieSettings   , makeSessionCookie   , makeSessionCookieBS+  , makeXsrfCookie   , makeCsrfCookie   , makeCookie   , makeCookieBS   , acceptLogin+  , clearSession     -- ** Related types   , IsSecure(..)-+  , SameSite(..)   , AreAuths    ----------------------------------------------------------------------------
src/Servant/Auth/Server/Internal.hs view
@@ -53,8 +53,8 @@        makeCookies :: AuthResult v -> IO (SetCookieList ('S ('S 'Z)))       makeCookies authResult = do-        csrf <- makeCsrfCookie cookieSettings-        fmap (Just csrf `SetCookieCons`) $+        xsrf <- makeXsrfCookie cookieSettings+        fmap (Just xsrf `SetCookieCons`) $           case authResult of             (Authenticated v) -> do               ejwt <- makeSessionCookie cookieSettings jwtSettings v
src/Servant/Auth/Server/Internal/ConfigTypes.hs view
@@ -1,4 +1,7 @@-module Servant.Auth.Server.Internal.ConfigTypes where+module Servant.Auth.Server.Internal.ConfigTypes+  ( module Servant.Auth.Server.Internal.ConfigTypes+  , Servant.API.IsSecure(..)+  ) where  import           Crypto.JOSE        as Jose import           Crypto.JWT         as Jose@@ -6,13 +9,11 @@ import           Data.Default.Class import           Data.Time import           GHC.Generics       (Generic)+import           Servant.API        (IsSecure(..))  data IsMatch = Matches | DoesNotMatch   deriving (Eq, Show, Read, Generic, Ord) -data IsSecure = Secure | NotSecure-  deriving (Eq, Show, Read, Generic, Ord)- data IsPasswordCorrect = PasswordCorrect | PasswordIncorrect   deriving (Eq, Show, Read, Generic, Ord) @@ -26,7 +27,13 @@  -- | @JWTSettings@ are used to generate cookies, and to verify JWTs. data JWTSettings = JWTSettings-  { key             :: Jose.JWK+  {+  -- | Key used to sign JWT.+    signingKey      :: Jose.JWK+  -- | Algorithm used to sign JWT.+  , jwtAlg          :: Maybe Jose.Alg+  -- | Keys used to validate JWT.+  , validationKeys  :: Jose.JWKSet   -- | An @aud@ predicate. The @aud@ is a string or URI that identifies the   -- intended recipient of the JWT.   , audienceMatches :: Jose.StringOrURI -> IsMatch@@ -34,7 +41,11 @@  -- | A @JWTSettings@ where the audience always matches. defaultJWTSettings :: Jose.JWK -> JWTSettings-defaultJWTSettings k = JWTSettings { key = k, audienceMatches = const Matches }+defaultJWTSettings k = JWTSettings+   { signingKey = k+   , jwtAlg = Nothing+   , validationKeys = Jose.JWKSet [k]+   , audienceMatches = const Matches }  -- | The policies to use when generating cookies. --@@ -48,25 +59,19 @@   {   -- | 'Secure' means browsers will only send cookies over HTTPS. Default:   -- @Secure@.-    cookieIsSecure    :: IsSecure+    cookieIsSecure    :: !IsSecure   -- | How long from now until the cookie expires. Default: @Nothing@.-  , cookieMaxAge      :: Maybe DiffTime+  , cookieMaxAge      :: !(Maybe DiffTime)   -- | At what time the cookie expires. Default: @Nothing@.-  , cookieExpires     :: Maybe UTCTime+  , cookieExpires     :: !(Maybe UTCTime)   -- | The URL path and sub-paths for which this cookie is used. Default @Just "/"@.-  , cookiePath        :: Maybe BS.ByteString+  , cookiePath        :: !(Maybe BS.ByteString)   -- | 'SameSite' settings. Default: @SameSiteLax@.-  , cookieSameSite    :: SameSite+  , cookieSameSite    :: !SameSite   -- | What name to use for the cookie used for the session.-  , sessionCookieName :: BS.ByteString-  -- | What name to use for the cookie used for CSRF protection.-  , xsrfCookieName    :: BS.ByteString-  -- | What path to use for the cookie used for CSRF protection. Default @Just "/"@.-  , xsrfCookiePath    :: Maybe BS.ByteString-  -- | What name to use for the header used for CSRF protection.-  , xsrfHeaderName    :: BS.ByteString-  -- | Exclude GET request method from CSRF protection.-  , xsrfExcludeGet    :: Bool+  , sessionCookieName :: !BS.ByteString+  -- | The optional settings to use for XSRF protection. Default @Just def@.+  , cookieXsrfSetting :: !(Maybe XsrfCookieSettings)   } deriving (Eq, Show, Generic)  instance Default CookieSettings where@@ -80,13 +85,33 @@     , cookiePath        = Just "/"     , cookieSameSite    = SameSiteLax     , sessionCookieName = "JWT-Cookie"-    , xsrfCookieName    = "XSRF-TOKEN"-    , xsrfCookiePath    = Just "/"-    , xsrfHeaderName    = "X-XSRF-TOKEN"-    , xsrfExcludeGet    = False+    , cookieXsrfSetting = Just def     } +-- | The policies to use when generating and verifying XSRF cookies+data XsrfCookieSettings = XsrfCookieSettings+  {+  -- | What name to use for the cookie used for XSRF protection.+    xsrfCookieName :: !BS.ByteString+  -- | What path to use for the cookie used for XSRF protection. Default @Just "/"@.+  , xsrfCookiePath :: !(Maybe BS.ByteString)+  -- | What name to use for the header used for XSRF protection.+  , xsrfHeaderName :: !BS.ByteString+  -- | Exclude GET request method from XSRF protection.+  , xsrfExcludeGet :: !Bool+  } deriving (Eq, Show, Generic) +instance Default XsrfCookieSettings where+  def = defaultXsrfCookieSettings++defaultXsrfCookieSettings :: XsrfCookieSettings+defaultXsrfCookieSettings = XsrfCookieSettings+  { xsrfCookieName = "XSRF-TOKEN"+  , xsrfCookiePath = Just "/"+  , xsrfHeaderName = "X-XSRF-TOKEN"+  , xsrfExcludeGet = False+  }+ ------------------------------------------------------------------------------ -- Internal {{{ @@ -94,6 +119,6 @@ jwtSettingsToJwtValidationSettings s   = defaultJWTValidationSettings (toBool <$> audienceMatches s)   where-    toBool Matches = True+    toBool Matches      = True     toBool DoesNotMatch = False -- }}}
src/Servant/Auth/Server/Internal/Cookie.hs view
@@ -7,11 +7,14 @@ import qualified Crypto.JWT               as Jose import           Crypto.Util              (constTimeEq) import qualified Data.ByteString          as BS+import qualified Data.ByteString.Char8    as BSC import qualified Data.ByteString.Base64   as BS64 import qualified Data.ByteString.Lazy     as BSL import           Data.CaseInsensitive     (mk)+import           Data.Maybe               (fromMaybe, isJust) import           Network.HTTP.Types       (methodGet)-import           Network.Wai              (requestHeaders, requestMethod)+import           Network.HTTP.Types.Header(hCookie)+import           Network.Wai              (Request, requestHeaders, requestMethod) import           Servant                  (AddHeader, addHeader) import           System.Entropy           (getEntropy) import           Web.Cookie@@ -26,18 +29,18 @@ cookieAuthCheck ccfg jwtCfg = do   req <- ask   jwtCookie <- maybe mempty return $ do-    cookies' <- lookup "Cookie" $ requestHeaders req+    cookies' <- lookup hCookie $ requestHeaders req     let cookies = parseCookies cookies'-    xsrfCookie <- lookup (xsrfCookieName ccfg) cookies-    when (((requestMethod req) /= methodGet) || not (xsrfExcludeGet ccfg)) $ do-      xsrfHeader <- lookup (mk $ xsrfHeaderName ccfg) $ requestHeaders req-      guard $ xsrfCookie `constTimeEq` xsrfHeader+    -- Apply the XSRF check if enabled.+    guard $ fromMaybe True $ do+      xsrfCookieCfg <- xsrfCheckRequired ccfg req+      return $ xsrfCookieAuthCheck xsrfCookieCfg req cookies     -- session cookie *must* be HttpOnly and Secure     lookup (sessionCookieName ccfg) cookies   verifiedJWT <- liftIO $ runExceptT $ do     unverifiedJWT <- Jose.decodeCompact $ BSL.fromStrict jwtCookie     Jose.verifyClaims (jwtSettingsToJwtValidationSettings jwtCfg)-                      (key jwtCfg)+                      (validationKeys jwtCfg)                       unverifiedJWT   case verifiedJWT of     Left (_ :: Jose.JWTError) -> mzero@@ -45,41 +48,80 @@       Left _ -> mzero       Right v' -> return v' --- | Makes a cookie to be used for CSRF.+xsrfCheckRequired :: CookieSettings -> Request -> Maybe XsrfCookieSettings+xsrfCheckRequired cookieSettings req = do+    xsrfCookieCfg <- cookieXsrfSetting cookieSettings+    let disableForGetReq = xsrfExcludeGet xsrfCookieCfg && requestMethod req == methodGet+    guard $ not disableForGetReq+    return xsrfCookieCfg++xsrfCookieAuthCheck :: XsrfCookieSettings -> Request -> [(BS.ByteString, BS.ByteString)] -> Bool+xsrfCookieAuthCheck xsrfCookieCfg req cookies = fromMaybe False $ do+  xsrfCookie <- lookup (xsrfCookieName xsrfCookieCfg) cookies+  xsrfHeader <- lookup (mk $ xsrfHeaderName xsrfCookieCfg) $ requestHeaders req+  return $ xsrfCookie `constTimeEq` xsrfHeader++-- | Makes a cookie to be used for XSRF.+makeXsrfCookie :: CookieSettings -> IO SetCookie+makeXsrfCookie cookieSettings = case cookieXsrfSetting cookieSettings of+  Just xsrfCookieSettings -> makeRealCookie xsrfCookieSettings+  Nothing                 -> return $ noXsrfTokenCookie cookieSettings+  where+    makeRealCookie xsrfCookieSettings = do+      xsrfValue <- BS64.encode <$> getEntropy 32+      return+        $ applyXsrfCookieSettings xsrfCookieSettings+        $ applyCookieSettings cookieSettings+        $ def{ setCookieValue = xsrfValue }+++-- | Alias for 'makeXsrfCookie'. makeCsrfCookie :: CookieSettings -> IO SetCookie-makeCsrfCookie cookieSettings = do-  csrfValue <- BS64.encode <$> getEntropy 32-  return $ def-    { setCookieName = xsrfCookieName cookieSettings-    , setCookieValue = csrfValue-    , setCookieMaxAge = cookieMaxAge cookieSettings-    , setCookieExpires = cookieExpires cookieSettings-    , setCookiePath = xsrfCookiePath cookieSettings-    , setCookieSecure = case cookieIsSecure cookieSettings of-        Secure -> True-        NotSecure -> False-    }+makeCsrfCookie = makeXsrfCookie+{-# DEPRECATED makeCsrfCookie "Use makeXsrfCookie instead" #-} + -- | Makes a cookie with session information. makeSessionCookie :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe SetCookie) makeSessionCookie cookieSettings jwtSettings v = do-  ejwt <- makeJWT v jwtSettings Nothing+  ejwt <- makeJWT v jwtSettings (cookieExpires cookieSettings)   case ejwt of     Left _ -> return Nothing-    Right jwt -> return $ Just $ def-      { setCookieName = sessionCookieName cookieSettings-      , setCookieValue = BSL.toStrict jwt-      , setCookieHttpOnly = True-      , setCookieMaxAge = cookieMaxAge cookieSettings-      , setCookieExpires = cookieExpires cookieSettings-      , setCookiePath = cookiePath cookieSettings-      , setCookieSecure = case cookieIsSecure cookieSettings of-          Secure -> True-          NotSecure -> False-      }+    Right jwt -> return+      $ Just+      $ applySessionCookieSettings cookieSettings+      $ applyCookieSettings cookieSettings+      $ def{ setCookieValue = BSL.toStrict jwt } +noXsrfTokenCookie :: CookieSettings -> SetCookie+noXsrfTokenCookie cookieSettings =+  applyCookieSettings cookieSettings $ def{ setCookieName = "NO-XSRF-TOKEN", setCookieValue = "" }++applyCookieSettings :: CookieSettings -> SetCookie -> SetCookie+applyCookieSettings cookieSettings setCookie = setCookie+  { setCookieMaxAge = cookieMaxAge cookieSettings+  , setCookieExpires = cookieExpires cookieSettings+  , setCookiePath = cookiePath cookieSettings+  , setCookieSecure = case cookieIsSecure cookieSettings of+      Secure -> True+      NotSecure -> False+  }++applyXsrfCookieSettings :: XsrfCookieSettings -> SetCookie -> SetCookie+applyXsrfCookieSettings xsrfCookieSettings setCookie = setCookie+  { setCookieName = xsrfCookieName xsrfCookieSettings+  , setCookiePath = xsrfCookiePath xsrfCookieSettings+  , setCookieHttpOnly = False+  }++applySessionCookieSettings :: CookieSettings -> SetCookie -> SetCookie+applySessionCookieSettings cookieSettings setCookie = setCookie+  { setCookieName = sessionCookieName cookieSettings+  , setCookieHttpOnly = True+  }+ -- | For a JWT-serializable session, returns a function that decorates a--- provided response object with CSRF and session cookies. This should be used+-- provided response object with XSRF and session cookies. This should be used -- when a user successfully authenticates with credentials. acceptLogin :: ( ToJWT session                , AddHeader "Set-Cookie" SetCookie response withOneCookie@@ -93,8 +135,21 @@   case mSessionCookie of     Nothing            -> pure Nothing     Just sessionCookie -> do-      csrfCookie <- makeCsrfCookie cookieSettings-      return $ Just $ addHeader sessionCookie . addHeader csrfCookie+      xsrfCookie <- makeXsrfCookie cookieSettings+      return $ Just $ addHeader sessionCookie . addHeader xsrfCookie++-- | Adds headers to a response that clears all session cookies.+clearSession :: ( AddHeader "Set-Cookie" SetCookie response withOneCookie+                , AddHeader "Set-Cookie" SetCookie withOneCookie withTwoCookies )+             => CookieSettings+             -> response+             -> withTwoCookies+clearSession cookieSettings = addHeader clearedSessionCookie . addHeader clearedXsrfCookie+  where+    clearedSessionCookie = applySessionCookieSettings cookieSettings $ applyCookieSettings cookieSettings def+    clearedXsrfCookie = case cookieXsrfSetting cookieSettings of+        Just xsrfCookieSettings -> applyXsrfCookieSettings xsrfCookieSettings $ applyCookieSettings cookieSettings def+        Nothing                 -> noXsrfTokenCookie cookieSettings  makeSessionCookieBS :: ToJWT v => CookieSettings -> JWTSettings -> v -> IO (Maybe BS.ByteString) makeSessionCookieBS a b c = fmap (toByteString . renderSetCookie)  <$> makeSessionCookie a b c
src/Servant/Auth/Server/Internal/JWT.hs view
@@ -11,6 +11,7 @@ import qualified Data.ByteString      as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.HashMap.Strict  as HM+import           Data.Maybe           (fromMaybe) import qualified Data.Text            as T import           Data.Time            (UTCTime) import           Network.Wai          (requestHeaders)@@ -56,7 +57,7 @@   verifiedJWT <- liftIO $ runExceptT $ do     unverifiedJWT <- Jose.decodeCompact $ BSL.fromStrict token     Jose.verifyClaims (jwtSettingsToJwtValidationSettings config)-                      (key config)+                      (validationKeys config)                       unverifiedJWT   case verifiedJWT of     Left (_ :: Jose.JWTError) -> mzero@@ -72,8 +73,9 @@ makeJWT :: ToJWT a   => a -> JWTSettings -> Maybe UTCTime -> IO (Either Jose.Error BSL.ByteString) makeJWT v cfg expiry = runExceptT $ do-  alg <- Jose.bestJWSAlg $ key cfg-  ejwt <- Jose.signClaims (key cfg)+  bestAlg <- Jose.bestJWSAlg $ signingKey cfg+  let alg = fromMaybe bestAlg $ jwtAlg cfg+  ejwt <- Jose.signClaims (signingKey cfg)                           (Jose.newJWSHeader ((), alg))                           (addExp $ encodeJWT v) 
src/Servant/Auth/Server/Internal/Types.hs view
@@ -3,7 +3,8 @@ import Control.Applicative import Control.Monad.Reader import Control.Monad.Time-import Data.Monoid+import Data.Monoid          (Monoid (..))+import Data.Semigroup       (Semigroup (..)) import Data.Time            (getCurrentTime) import GHC.Generics         (Generic) import Network.Wai          (Request)@@ -21,10 +22,13 @@   | Indefinite   deriving (Eq, Show, Read, Generic, Ord, Functor, Traversable, Foldable) +instance Semigroup (AuthResult val) where+  Indefinite <> y = y+  x          <> _ = x+ instance Monoid (AuthResult val) where   mempty = Indefinite-  Indefinite `mappend` x = x-  x `mappend` _ = x+  mappend = (<>)  instance Applicative AuthResult where   pure = return@@ -54,12 +58,15 @@   { runAuthCheck :: Request -> IO (AuthResult val) }   deriving (Generic, Functor) -instance Monoid (AuthCheck val) where-  mempty = AuthCheck $ const $ return mempty-  AuthCheck f `mappend` AuthCheck g = AuthCheck $ \x -> do+instance Semigroup (AuthCheck val) where+  AuthCheck f <> AuthCheck g = AuthCheck $ \x -> do     fx <- f x     gx <- g x     return $ fx <> gx++instance Monoid (AuthCheck val) where+  mempty = AuthCheck $ const $ return mempty+  mappend = (<>)  instance Applicative AuthCheck where   pure = return
test/Servant/Auth/ServerSpec.hs view
@@ -3,6 +3,7 @@  import           Control.Lens import           Control.Monad.Except                (runExceptT)+import           Control.Monad.IO.Class              (liftIO) import           Crypto.JOSE                         (Alg (HS256, None), Error,                                                       JWK, JWSHeader,                                                       KeyMaterialGenParam (OctGenParam),@@ -16,7 +17,7 @@                                                       emptyClaimsSet,                                                       unregisteredClaims) import           Data.Aeson                          (FromJSON, ToJSON, Value,-                                                      toJSON)+                                                      toJSON, encode) import           Data.Aeson.Lens                     (_JSON) import qualified Data.ByteString                     as BS import qualified Data.ByteString.Lazy                as BSL@@ -34,12 +35,13 @@ import           Network.Wai.Handler.Warp            (testWithApplication) import           Network.Wreq                        (Options, auth, basicAuth,                                                       cookieExpiryTime, cookies,-                                                      defaults, get, getWith,+                                                      defaults, get, getWith, postWith,                                                       header, oauth2Bearer,                                                       responseBody,                                                       responseCookieJar,                                                       responseHeader,                                                       responseStatus)+import           Network.Wreq.Types                  (Postable(..)) import           Servant                             hiding (BasicAuth,                                                       IsSecure (..), header) import           Servant.Auth.Server@@ -47,12 +49,7 @@ import           System.IO.Unsafe                    (unsafePerformIO) import           Test.Hspec import           Test.QuickCheck--#if MIN_VERSION_http_client(0,5,0) import qualified Network.HTTP.Client as HCli-#else-import Network.HTTP.Client (HttpException (StatusCodeException))-#endif   @@ -115,40 +112,40 @@     it "sets cookies that it itself accepts" $ \port -> property $ \user -> do       jwt <- createJWT theKey (newJWSHeader ((), HS256))         (claims $ toJSON user)-      opts' <- addJwtToCookie jwt-      let opts = addCookie (opts' & header (mk (xsrfHeaderName cookieCfg)) .~ ["blah"])-                           (xsrfCookieName cookieCfg <> "=blah")+      opts' <- addJwtToCookie cookieCfg jwt+      let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])+                           (xsrfField xsrfCookieName cookieCfg <> "=blah")       resp <- getWith opts (url port)       let (cookieJar:_) = resp ^.. responseCookieJar-          Just xxsrf = find (\x -> cookie_name x == xsrfCookieName cookieCfg)+          Just xxsrf = find (\x -> cookie_name x == xsrfField xsrfCookieName cookieCfg)                      $ destroyCookieJar cookieJar           opts2 = defaults             & cookies .~ Just cookieJar-            & header (mk (xsrfHeaderName cookieCfg)) .~ [cookie_value xxsrf]+            & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ [cookie_value xxsrf]       resp2 <- getWith opts2 (url port)       resp2 ^? responseBody . _JSON `shouldBe` Just (length $ name user)      it "uses the Expiry from the configuration" $ \port -> property $ \(user :: User) -> do       jwt <- createJWT theKey (newJWSHeader ((), HS256))         (claims $ toJSON user)-      opts' <- addJwtToCookie jwt-      let opts = addCookie (opts' & header (mk (xsrfHeaderName cookieCfg)) .~ ["blah"])-                           (xsrfCookieName cookieCfg <> "=blah")+      opts' <- addJwtToCookie cookieCfg jwt+      let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])+                           (xsrfField xsrfCookieName cookieCfg <> "=blah")       resp <- getWith opts (url port)       let (cookieJar:_) = resp ^.. responseCookieJar-          Just xxsrf = find (\x -> cookie_name x == xsrfCookieName cookieCfg)+          Just xxsrf = find (\x -> cookie_name x == xsrfField xsrfCookieName cookieCfg)                      $ destroyCookieJar cookieJar       xxsrf ^. cookieExpiryTime `shouldBe` future      it "sets the token cookie as HttpOnly" $ \port -> property $ \(user :: User) -> do       jwt <- createJWT theKey (newJWSHeader ((), HS256))         (claims $ toJSON user)-      opts' <- addJwtToCookie jwt-      let opts = addCookie (opts' & header (mk (xsrfHeaderName cookieCfg)) .~ ["blah"])-                           (xsrfCookieName cookieCfg <> "=blah")+      opts' <- addJwtToCookie cookieCfg jwt+      let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])+                           (xsrfField xsrfCookieName cookieCfg <> "=blah")       resp <- getWith opts (url port)       let (cookieJar:_) = resp ^.. responseCookieJar-          Just token = find (\x -> cookie_name x == "JWT-Cookie")+          Just token = find (\x -> cookie_name x == sessionCookieName cookieCfg)                      $ destroyCookieJar cookieJar       cookie_http_only token `shouldBe` True @@ -161,42 +158,120 @@ cookieAuthSpec :: Spec cookieAuthSpec   = describe "The Auth combinator" $ do-      around (testWithApplication . return $ app cookieOnlyApi) $ do+      describe "With XSRF check" $+       around (testWithApplication . return $ app cookieOnlyApi) $ do -        it "fails if CSRF header and cookie don't match" $ \port -> property+        it "fails if XSRF header and cookie don't match" $ \port -> property                                                          $ \(user :: User) -> do           jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)-          opts' <- addJwtToCookie jwt-          let opts = addCookie (opts' & header (mk (xsrfHeaderName cookieCfg)) .~ ["blah"])-                               (xsrfCookieName cookieCfg <> "=blerg")+          opts' <- addJwtToCookie cookieCfg jwt+          let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])+                               (xsrfField xsrfCookieName cookieCfg <> "=blerg")           getWith opts (url port) `shouldHTTPErrorWith` status401 -        it "fails if there is no CSRF header and cookie" $ \port -> property+        it "fails with no XSRF header or cookie" $ \port -> property                                                          $ \(user :: User) -> do           jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)-          opts <- addJwtToCookie jwt+          opts <- addJwtToCookie cookieCfg jwt           getWith opts (url port) `shouldHTTPErrorWith` status401 -        it "succeeds if CSRF header and cookie match, and JWT is valid" $ \port -> property-                                                                       $ \(user :: User) -> do+        it "succeeds if XSRF header and cookie match, and JWT is valid" $ \port -> property+                                                                        $ \(user :: User) -> do           jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)-          opts' <- addJwtToCookie jwt-          let opts = addCookie (opts' & header (mk (xsrfHeaderName cookieCfg)) .~ ["blah"])-                               (xsrfCookieName cookieCfg <> "=blah")+          opts' <- addJwtToCookie cookieCfg jwt+          let opts = addCookie (opts' & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ ["blah"])+                               (xsrfField xsrfCookieName cookieCfg <> "=blah")           resp <- getWith opts (url port)           resp ^? responseBody . _JSON `shouldBe` Just (length $ name user) -      around (testWithApplication . return $ appWithCookie cookieOnlyApi cookieCfg{xsrfExcludeGet = True}) $ do-        it "succeeds without CSRF header for GET when enabled, and JWT is valid"-              $ \port -> property-              $ \(user :: User) -> do+        it "sets and clears the right cookies" $ \port -> property+                                               $ \(user :: User) -> do+          let optsFromResp resp =+                let jar = resp ^. responseCookieJar+                    Just xsrfCookieValue = cookie_value <$> find (\c -> cookie_name c == xsrfField xsrfCookieName cookieCfg) (destroyCookieJar jar)+                in defaults+                    & cookies .~ Just jar -- real cookie jars aren't updated by being replaced+                    & header (mk (xsrfField xsrfHeaderName cookieCfg)) .~ [xsrfCookieValue]++          resp <- postWith defaults (url port ++ "/login") user+          (resp ^. responseCookieJar) `shouldMatchCookieNames`+            [ sessionCookieName cookieCfg+            , xsrfField xsrfCookieName cookieCfg+            ]+          let loggedInOpts = optsFromResp resp++          resp <- getWith loggedInOpts (url port)+          resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)++          resp <- getWith loggedInOpts (url port ++ "/logout")+          (resp ^. responseCookieJar) `shouldMatchCookieNameValues`+            [ (sessionCookieName cookieCfg, "value")+            , (xsrfField xsrfCookieName cookieCfg, "value")+            ]+          let loggedOutOpts = optsFromResp resp++          getWith loggedOutOpts (url port) `shouldHTTPErrorWith` status401++      describe "With no XSRF check for GET requests" $ let+            noXsrfGet xsrfCfg = xsrfCfg { xsrfExcludeGet = True }+            cookieCfgNoXsrfGet = cookieCfg { cookieXsrfSetting = fmap noXsrfGet $ cookieXsrfSetting cookieCfg }+       in around (testWithApplication . return $ appWithCookie cookieOnlyApi cookieCfgNoXsrfGet) $ do++        it "succeeds with no XSRF header or cookie for GET" $ \port -> property+                                                            $ \(user :: User) -> do           jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)-          opts' <- addJwtToCookie jwt-          let opts = addCookie opts' (xsrfCookieName cookieCfg <> "=blah")+          opts <- addJwtToCookie cookieCfgNoXsrfGet jwt           resp <- getWith opts (url port)           resp ^? responseBody . _JSON `shouldBe` Just (length $ name user) +        it "fails with no XSRF header or cookie for POST" $ \port -> property+                                                          $ \(user :: User) number -> do+          jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)+          opts <- addJwtToCookie cookieCfgNoXsrfGet jwt+          postWith opts (url port) (toJSON (number :: Int)) `shouldHTTPErrorWith` status401 +      describe "With no XSRF check at all" $ let+            cookieCfgNoXsrf = cookieCfg { cookieXsrfSetting = Nothing }+       in around (testWithApplication . return $ appWithCookie cookieOnlyApi cookieCfgNoXsrf) $ do++        it "succeeds with no XSRF header or cookie for GET" $ \port -> property+                                                            $ \(user :: User) -> do+          jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)+          opts <- addJwtToCookie cookieCfgNoXsrf jwt+          resp <- getWith opts (url port)+          resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)++        it "succeeds with no XSRF header or cookie for POST" $ \port -> property+                                                             $ \(user :: User) number -> do+          jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)+          opts <- addJwtToCookie cookieCfgNoXsrf jwt+          resp <- postWith opts (url port) $ toJSON (number :: Int)+          resp ^? responseBody . _JSON `shouldBe` Just number++        it "sets and clears the right cookies" $ \port -> property+                                               $ \(user :: User) -> do+          let optsFromResp resp = defaults+                & cookies .~ Just (resp ^. responseCookieJar) -- real cookie jars aren't updated by being replaced++          resp <- postWith defaults (url port ++ "/login") user+          (resp ^. responseCookieJar) `shouldMatchCookieNames`+            [ sessionCookieName cookieCfg+            , "NO-XSRF-TOKEN"+            ]+          let loggedInOpts = optsFromResp resp++          resp <- getWith (loggedInOpts) (url port)+          resp ^? responseBody . _JSON `shouldBe` Just (length $ name user)++          resp <- getWith loggedInOpts (url port ++ "/logout")+          (resp ^. responseCookieJar) `shouldMatchCookieNameValues`+            [ (sessionCookieName cookieCfg, "value")+            , ("NO-XSRF-TOKEN", "")+            ]+          let loggedOutOpts = optsFromResp resp++          getWith loggedOutOpts (url port) `shouldHTTPErrorWith` status401+ -- }}} ------------------------------------------------------------------------------ -- * JWT Auth {{{@@ -315,6 +390,10 @@        :<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)        :<|> "raw" :> Raw         )+      :<|> "login" :> ReqBody '[JSON] User :> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie+                                                                     , Header "Set-Cookie" SetCookie ] NoContent)+      :<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie+                                             , Header "Set-Cookie" SetCookie ] NoContent)  jwtOnlyApi :: Proxy (API '[Servant.Auth.Server.JWT]) jwtOnlyApi = Proxy@@ -335,11 +414,16 @@  cookieCfg :: CookieSettings cookieCfg = def-  { xsrfCookieName = "TheyDinedOnMince"-  , xsrfHeaderName = "AndSlicesOfQuince"-  , cookieExpires = Just future+  { cookieExpires = Just future   , cookieIsSecure = NotSecure+  , sessionCookieName = "RuncibleSpoon"+  , cookieXsrfSetting = pure $ def+    { xsrfCookieName = "TheyDinedOnMince"+    , xsrfHeaderName = "AndSlicesOfQuince"+    }   }+xsrfField :: (XsrfCookieSettings -> a) -> CookieSettings -> a+xsrfField f = maybe (error "expected XsrfCookieSettings for test") f . cookieXsrfSetting  jwtCfg :: JWTSettings jwtCfg = (defaultJWTSettings theKey) { audienceMatches = \x ->@@ -357,7 +441,7 @@  appWithCookie :: AreAuths auths '[CookieSettings, JWTSettings, JWK] User   => Proxy (API auths) -> CookieSettings -> Application-appWithCookie api ccfg = serveWithContext api ctx server+appWithCookie api ccfg = serveWithContext api ctx $ server ccfg   where     ctx = ccfg :. jwtCfg :. theKey :. EmptyContext @@ -366,14 +450,18 @@   => Proxy (API auths) -> Application app api = appWithCookie api cookieCfg -server :: Server (API auths)-server authResult = case authResult of-  Authenticated usr -> getInt usr-                  :<|> postInt usr-                  :<|> getHeaderInt-                  :<|> raw-  Indefinite -> throwAll err401-  _ -> throwAll err403+server :: CookieSettings -> Server (API auths)+server ccfg =+    (\authResult -> case authResult of+        Authenticated usr -> getInt usr+                        :<|> postInt usr+                        :<|> getHeaderInt+                        :<|> raw+        Indefinite -> throwAll err401+        _ -> throwAll err403+    )+    :<|> getLogin+    :<|> getLogout   where     getInt :: User -> Handler Int     getInt usr = return . length $ name usr@@ -384,6 +472,16 @@     getHeaderInt :: Handler (Headers '[Header "Blah" Int] Int)     getHeaderInt = return $ addHeader 1797 17 +    getLogin :: User -> Handler (Headers '[ Header "Set-Cookie" SetCookie+                                          , Header "Set-Cookie" SetCookie ] NoContent)+    getLogin user = do+        Just applyCookies <- liftIO $ acceptLogin ccfg jwtCfg user+        return $ applyCookies NoContent++    getLogout :: Handler (Headers '[ Header "Set-Cookie" SetCookie+                                   , Header "Set-Cookie" SetCookie ] NoContent)+    getLogout = return $ clearSession ccfg NoContent+     raw :: Server Raw     raw = #if MIN_VERSION_servant_server(0,11,0)@@ -411,11 +509,11 @@ createJWT :: JWK -> JWSHeader () -> ClaimsSet -> IO (Either Error Crypto.JWT.SignedJWT) createJWT k a b = runExceptT $ signClaims k a b -addJwtToCookie :: ToCompact a => Either Error a -> IO Options-addJwtToCookie jwt = case jwt >>= (return . encodeCompact) of+addJwtToCookie :: ToCompact a => CookieSettings -> Either Error a -> IO Options+addJwtToCookie ccfg jwt = case jwt >>= (return . encodeCompact) of   Left e -> fail $ show e   Right v -> return-    $ defaults & header "Cookie" .~ ["JWT-Cookie=" <> BSL.toStrict v]+    $ defaults & header "Cookie" .~ [sessionCookieName ccfg <> "=" <> BSL.toStrict v]  addCookie :: Options -> BS.ByteString -> Options addCookie opts cookie' = opts & header "Cookie" %~ \c -> case c of@@ -430,10 +528,20 @@   HCli.HttpExceptionRequest _ (HCli.StatusCodeException resp _)     -> HCli.responseStatus resp == stat #else-  StatusCodeException x _ _ -> x == stat+  HCli.StatusCodeException x _ _ -> x == stat #endif   _ -> False +shouldMatchCookieNames :: HCli.CookieJar -> [BS.ByteString] -> Expectation+shouldMatchCookieNames cj patterns+    = fmap cookie_name (destroyCookieJar cj)+    `shouldMatchList` patterns++shouldMatchCookieNameValues :: HCli.CookieJar -> [(BS.ByteString, BS.ByteString)] -> Expectation+shouldMatchCookieNameValues cj patterns+    = fmap ((,) <$> cookie_name <*> cookie_value) (destroyCookieJar cj)+    `shouldMatchList` patterns+ url :: Int -> String url port = "http://localhost:" <> show port @@ -455,5 +563,12 @@  instance Arbitrary User where   arbitrary = User <$> arbitrary <*> arbitrary++instance Postable User where+  postPayload user request = return $ request+    { HCli.requestBody = HCli.RequestBodyLBS $ encode user+    , HCli.requestHeaders = (mk "Content-Type", "application/json") : HCli.requestHeaders request+    }+  -- }}}