Closed seanhess closed 5 years ago
You'll need to setup XSRF or disable it.
@domenkozar The server is sending down the XSRF cookie and the browser is sending it back. (See the Cookie header I pasted, above, that’s exactly what the browser is sending).
Is there something else I need to do? If so, what? The readme seems to imply that one must implement something on the client to handle XSRF, but I think it’s already configured correctly
I get the same issue when running the example exactly as written in the README. Code is below. Tested via Postman. I can login, and it registers the cookies, but I still get 401 Unauthorized when I try to view the protected resources.
The only change is setting the cookie settings to cookieIsSecure = NotSecure
so that I can test locally. Could that be the problem? If so, how can you test it?
module AuthTest where
{-# 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 Auth (auths :: [*]) val
data AuthResult val
= BadPassword
| NoSuchUser
| Authenticated val
| Indefinite
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
-- 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"
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
cokCfg = defaultCookieSettings { cookieIsSecure = NotSecure }
cfg = cokCfg :. jwtCfg :. EmptyContext
--- Here is the actual change
api = Proxy :: Proxy (API '[Cookie])
run 7249 $ serveWithContext api cfg (server cokCfg 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
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
Run via
$ stack ghci
> mainWithCookies
How are you requesting protected endpoint? From README: 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
My recommendation would be to turn off CSRF and rely https://caniuse.com/#search=samesite instead (which is already set by default in servant-auth)
Thanks @domenkozar. I misunderstood how XSRF works. I thought it was automatic, but as you said, one must set the X-XSRF-TOKEN header. Since my browser was including the XSRF token in the cookie header I thought I was doing it correctly.
The docs are fairly clear, I read too quickly. I think they might be improved by adding a curl example for cookies. (It would have been easier to see I needed to set that header, and to compare my requests to the one generated by curl)
Also, your advice says you shouldn't disable XSRF if you use Javascript, but my reading suggests that the Same-Site cookie setting is secure for most applications, and supersedes XSRF tokens. It seems like leaving XSRF tokens disabled and same-site on might be a good default setting. I'm not an expert though.
Other documentation suggestions: the way you're using a literate haskell file as your readme is cool, but it doesn't display on mobile. Even worse, it doesn't get picked up by hackage, so it's hard to discover. Compare https://hackage.haskell.org/package/servant-auth-server to http://hackage.haskell.org/package/selda. You can see the README on the hackage page in the latter.
Thanks for your hard work!
I have a similar problem as described by @seanhess. I am able to access the protected resource using curl but not with the browser. FYI I have disabled XSRF
and using NotSecure
in the defaultCookieSettings
for my tests. I suppose that is all that is required for the browser to access the protected resource. In my case this is not working. I get the AuthResult
Indefinite
My question is, do I have to set the header in the browser (client) i.e. "Authorization: Bearer <token>
" for this to work (to access protected resource)?
P.S. <token>
refers to the JWT-Cookie
in Set-Cookie
header.
A copy of the request and response headers are shown below:
Is there a way to access the request headers from the server side? I use Wai's RequestLogger
to log requests in development. However, I do not see cookie headers in these logs.
You can also temporarily add Header "header name" Text
to one of your handlers and print it from there. But using a request logger should work, or even a hacky little Middleware
like debug :: Middleware ; debug app req resp = do { putStrLn "Request headers:" ; print (requestHeaders req) ; app req resp }
that you can apply to your application by just doing run <port> $ debug (serve api yourServer)
. Or something like wireshark, if needs be...
I have been able to solve the issue of logging the request cookie headers by using a custom middleware. Through the middleware the request object which contains the request headers becomes available. I am able to access all the headers. @alpmestan thanks for your suggestions.
I've followed the README carefully, and my server successfully sets the cookies on a login. I can also access an authenticated resource with curl using the
Authorization:
headerHowever, for cookies to work, shouldn't it work with the
Cookie:
header? When I test this in the browser, it sends the following header, but I can't access the protected resource (I'm getting Indefinite as my AuthResult).Again, it does work if I use the
Authorization
header as shown in the README, but the browser doesn't set that header automatically.What am I missing? Thanks!