@@ -1,4 +1,5 @@
11{-# LANGUAGE CPP #-}
2+{-# LANGUAGE TypeApplications #-}
23module Servant.Auth.ServerSpec (spec) where
34
45#if !MIN_VERSION_servant_server(0,16,0)
@@ -24,6 +25,7 @@ import Data.Aeson (FromJSON, ToJSON, Value,
2425import Data.Aeson.Lens (_JSON)
2526import qualified Data.ByteString as BS
2627import qualified Data.ByteString.Lazy as BSL
28+import Data.Text (Text, pack)
2729import Data.CaseInsensitive (mk)
2830import Data.Foldable (find)
2931import Data.Monoid
@@ -39,6 +41,7 @@ import Network.HTTP.Types (Status, status200,
3941import Network.Wai (responseLBS)
4042import Network.Wai.Handler.Warp (testWithApplication)
4143import Network.Wreq (Options, auth, basicAuth,
44+ checkResponse,
4245 cookieExpiryTime, cookies,
4346 defaults, get, getWith, postWith,
4447 header, oauth2Bearer,
@@ -182,8 +185,21 @@ cookieAuthSpec
182185 it "fails with no XSRF header or cookie" $ \port -> property
183186 $ \(user :: User) -> do
184187 jwt <- createJWT theKey (newJWSHeader ((), HS256)) (claims $ toJSON user)
185− opts <- addJwtToCookie cookieCfg jwt
186− getWith opts (url port) `shouldHTTPErrorWith` status401
188+ opts' <- addJwtToCookie cookieCfg jwt
189+ let opts = opts' & checkResponse .~ Just mempty
190+ resp <- getWith opts (url port)
191+ resp ^. responseStatus `shouldBe` status401
192+ (resp ^. responseCookieJar) `shouldNotHaveCookies` ["XSRF-TOKEN"]
193+
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:
197+ --
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"]
202+
187203
188204 it "succeeds if XSRF header and cookie match, and JWT is valid" $ \port -> property
189205 $ \(user :: User) -> do
@@ -405,13 +421,14 @@ type API auths
405421 = Auth auths User :>
406422 ( Get '[JSON] Int
407423 :<|> ReqBody '[JSON] Int :> Post '[JSON] Int
408− :<|> NamedRoutes DummyRoutes
424+ :<|> "named" :> NamedRoutes DummyRoutes
409425 :<|> "header" :> Get '[JSON] (Headers '[Header "Blah" Int] Int)
410426#if MIN_VERSION_servant_server(0,15,0)
411427 :<|> "stream" :> StreamGet NoFraming OctetStream (SourceIO BS.ByteString)
412428#endif
413429 :<|> "raw" :> Raw
414430 )
431+ :<|> "uverb" :> Auth auths User :> UVerb 'GET '[JSON] '[WithStatus 200 Int, WithStatus 401 Text, WithStatus 403 Text]
415432 :<|> "login" :> ReqBody '[JSON] User :> Post '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
416433 , Header "Set-Cookie" SetCookie ] NoContent)
417434 :<|> "logout" :> Get '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
@@ -489,6 +506,11 @@ server ccfg =
489506 :<|> raw
490507 Indefinite -> throwAll err401
491508 _ -> throwAll err403
509+ ) :<|>
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")
492514 )
493515 :<|> getLogin
494516 :<|> getLogout
@@ -569,6 +591,12 @@ shouldMatchCookieNames cj patterns
569591 = fmap cookie_name (destroyCookieJar cj)
570592 `shouldMatchList` patterns
571593
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
599+
572600shouldMatchCookieNameValues :: HCli.CookieJar -> [(BS.ByteString, BS.ByteString)] -> Expectation
573601shouldMatchCookieNameValues cj patterns
574602 = fmap ((,) <$> cookie_name <*> cookie_value) (destroyCookieJar cj)