@@ -1,4 +1,5 @@
1{-# LANGUAGE CPP #-}
1{-# LANGUAGE CPP #-}
2+{-# LANGUAGE TypeApplications #-}
2module Servant.Auth.ServerSpec (spec) where
3module Servant.Auth.ServerSpec (spec) where
4#if !MIN_VERSION_servant_server(0,16,0)
5#if !MIN_VERSION_servant_server(0,16,0)
@@ -24,6 +25,7 @@ import Data.Aeson (FromJSON, ToJSON, Value,
24import Data.Aeson.Lens (_JSON)
25import Data.Aeson.Lens (_JSON)
25import qualified Data.ByteString as BS
26import qualified Data.ByteString as BS
26import qualified Data.ByteString.Lazy as BSL
27import qualified Data.ByteString.Lazy as BSL
28+import Data.Text (Text, pack)
27import Data.CaseInsensitive (mk)
29import Data.CaseInsensitive (mk)
28import Data.Foldable (find)
30import Data.Foldable (find)
29import Data.Monoid
31import Data.Monoid
@@ -39,6 +41,7 @@ import Network.HTTP.Types (Status, status200,
39import Network.Wai (responseLBS)
41import Network.Wai (responseLBS)
40import Network.Wai.Handler.Warp (testWithApplication)
42import Network.Wai.Handler.Warp (testWithApplication)
41import Network.Wreq (Options, auth, basicAuth,
43import Network.Wreq (Options, auth, basicAuth,
42 cookieExpiryTime, cookies,
45 cookieExpiryTime, cookies,
43 defaults, get, getWith, postWith,
46 defaults, get, getWith, postWith,
44 header, oauth2Bearer,
47 header, oauth2Bearer,
@@ -182,8 +185,21 @@ cookieAuthSpec
182 it "fails with no XSRF header or cookie" $ \port -> property
185 it "fails with no XSRF header or cookie" $ \port -> property
183 $ \(user :: User) -> do
186 $ \(user :: User) -> do
184 jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
187 jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
185− opts <- addJwtToCookie cookieCfg jwt
188+ opts' <- addJwtToCookie cookieCfg jwt
189+ let opts = opts' & checkResponse .~ Just mempty
186− getWith opts (url port) `shouldHTTPErrorWith` status401
190+ resp <- getWith opts (url port)
191+ resp ^. responseStatus `shouldBe` status401
192+ (resp ^. responseCookieJar) `shouldNotHaveCookies` ["XSRF-TOKEN"]
194+ -- Validating that the XSRF cookie isn't added for UVerb routes either.
195+ -- These routes can return a 401 response directly without using throwError / throwAll,
196+ -- which revealed a bug:
198+ -- https://github.com/haskell-servant/servant/issues/1570#issuecomment-1076374449
199+ resp <- getWith opts (url port ++ "/uverb")
200+ resp ^. responseStatus `shouldBe` status401
201+ (resp ^. responseCookieJar) `shouldNotHaveCookies` ["XSRF-TOKEN"]
188 it "succeeds if XSRF header and cookie match, and JWT is valid" $ \port -> property
204 it "succeeds if XSRF header and cookie match, and JWT is valid" $ \port -> property
189 $ \(user :: User) -> do
205 $ \(user :: User) -> do
@@ -405,13 +421,14 @@ type API auths
405 = Auth auths User :>
421 = Auth auths User :>
406 ( Get '[JSON] Int
422 ( Get '[JSON] Int
407 :<|> ReqBody '[JSON] Int :> Post '[JSON] Int
423 :<|> ReqBody '[JSON] Int :> Post '[JSON] Int
408− :<|> NamedRoutes DummyRoutes
424+ :<|> "named" :> NamedRoutes DummyRoutes
409 :<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)
425 :<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)
410#if MIN_VERSION_servant_server(0,15,0)
426#if MIN_VERSION_servant_server(0,15,0)
411 :<|> "stream" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
427 :<|> "stream" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
413 :<|> "raw" :> Raw
429 :<|> "raw" :> Raw
431+ :<|> "uverb" :> Auth auths User :> UVerb 'GET '[JSON] '[WithStatus 200 Int, WithStatus 401 Text, WithStatus 403 Text]
415 :<|> "login" :> ReqBody '[JSON] User :> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
432 :<|> "login" :> ReqBody '[JSON] User :> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
416 , Header "Set-Cookie" SetCookie ] NoContent)
433 , Header "Set-Cookie" SetCookie ] NoContent)
417 :<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
434 :<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
@@ -489,6 +506,11 @@ server ccfg =
490 Indefinite -> throwAll err401
507 Indefinite -> throwAll err401
491 _ -> throwAll err403
508 _ -> throwAll err403
510+ (\authResult -> case authResult of
511+ Authenticated usr -> respond (WithStatus @200 (42 :: Int))
512+ Indefinite -> respond (WithStatus @401 $ pack "Authentication required")
513+ _ -> respond (WithStatus @403 $ pack "Forbidden")
493 :<|> getLogin
515 :<|> getLogin
494 :<|> getLogout
516 :<|> getLogout
@@ -569,6 +591,12 @@ shouldMatchCookieNames cj patterns
569 = fmap cookie_name (destroyCookieJar cj)
591 = fmap cookie_name (destroyCookieJar cj)
570 `shouldMatchList` patterns
592 `shouldMatchList` patterns
594+shouldNotHaveCookies :: HCli.CookieJar -> [BS.ByteString] -> Expectation
595+shouldNotHaveCookies cj patterns =
596+ sequence_ $ (\cookieName -> cookieNames `shouldNotContain` [cookieName]) <$> patterns
597+ where cookieNames :: [BS.ByteString]
598+ cookieNames = cookie_name <$> destroyCookieJar cj
572shouldMatchCookieNameValues :: HCli.CookieJar -> [(BS.ByteString, BS.ByteString)] -> Expectation
600shouldMatchCookieNameValues :: HCli.CookieJar -> [(BS.ByteString, BS.ByteString)] -> Expectation
573shouldMatchCookieNameValues cj patterns
601shouldMatchCookieNameValues cj patterns
574 = fmap ((,) <$> cookie_name <*> cookie_value) (destroyCookieJar cj)
602 = fmap ((,) <$> cookie_name <*> cookie_value) (destroyCookieJar cj)