haskell-servant / servant-auth

160 stars 73 forks source link

Missing part in readme: Can't successfully use cookie from browser #166

Open bratfizyk opened 4 years ago

bratfizyk commented 4 years ago

I've been trying to implement a very simple server with even simpler Lucid client that would work with Cookie auth server presented in this project's Readme. However, based on the information found there I'm unable to implement this. The problem is that I always get "AuthResult Indefinite" instead of "Authenticated" even though I get the cookie and my browser manages to process it. I guess I must be missing a tiny part, but not sure which one.

I've got a simple api, as suggested in the tutorial:

data User = User String
    deriving (Eq, Show, Generic)

data Credentials = Credentials {
    credentialsUserName :: String,
    credentialsPassword :: String
} deriving (Eq, Show, Read, Generic)

type Unprotected =
    "logMe" :> Get '[HTML] (Html ())
    :<|> "login" 
            :> ReqBody '[FormUrlEncoded] Credentials 
            :> Verb 'POST 204 '[JSON] (Headers '[ Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent)

type Protected
   = "name" :> Get '[JSON] String

type AuthAPI =
    (Servant.Auth.Server.Auth '[Cookie] User :> Protected)
    :<|> Unprotected

I added the "logMe" endpoint that contains a simple form that works with "Credentials" type. This part of code most likely works ok, as I get a cookie in my browser. image

Then, when I try poking the "name" endpoint, I get "Indefinite" result. I guess my project is missing a part that does cookie to User conversion, but I'm not sure how to add this. Any hints ?

Other relevant pieces of my code (very similar to the content of Readme):

cookieConfig :: CookieSettings
cookieConfig = defaultCookieSettings { cookieIsSecure = NotSecure }

getJwtConfig :: IO JWTSettings
getJwtConfig = do
    key <- generateKey 
    return $ defaultJWTSettings key

context :: CookieSettings -> JWTSettings -> (Context '[CookieSettings, JWTSettings])
context cookieConfig jwtConfig = cookieConfig :. jwtConfig :. EmptyContext

protected :: Servant.Auth.Server.AuthResult User -> Server Protected
protected (Servant.Auth.Server.Authenticated (User name)) = return name
protected x = trace ("Access Denied " ++ (show x)) $ throwAll err401

checkCreds :: CookieSettings -> JWTSettings  -> Credentials 
                                -> Handler (Headers '[ Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent)
checkCreds cookieSettings jwtSettings (Credentials { credentialsUserName = "Ali Baba", credentialsPassword = "Open Sesame"}) = do
    mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings (User "Ali Baba")
    case mApplyCookies of
        Nothing           -> trace "Nothing" $ throwError err401
        Just applyCookies -> return $ applyCookies NoContent

checkCreds _ _ (Credentials { credentialsUserName = user, credentialsPassword = _}) = 
    trace ("Received " ++ user)
        throwError err401

main :: IO ()
main = do
    migrateDB
    jwtConfig <- getJwtConfig
    putStrLn $ "Serving endpoint " ++ (show port)
    run port $ serveWithContext proxy (context cookieConfig jwtConfig) (appAPI cookieConfig jwtConfig)

Dislaimer: I'm quite new to Haskell, so it might be the case that I'm missing an important concept here.

domenkozar commented 4 years ago

It's since XSRF also checks GET requests (terrible default). I recommend disabling XSRF and setting cookieSameSite setting to SameSiteStrict

bratfizyk commented 4 years ago

I tried

cookieConfig :: CookieSettings
cookieConfig = defaultCookieSettings { cookieIsSecure = NotSecure, cookieSameSite = SameSiteStrict, cookieXsrfSetting = Nothing }

and it did the job, thanks. Over the weekend I'll think about a few other features (e.g. how to delete a cookie to log user out) and will come up with a PR for the README file so that this use case becomes more obvious.