Search code examples
javascriptauthenticationhaskellcookiesservant

Logging out with Servant (Haskell)


I've been trying to implement a simple server with cookie-based authentication using Servant. I found an example here.

Server

I created my API:

type API
    = Auth '[Cookie] User :>
      "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)

Here's an implementation for the endpoints:

checkCreds :: CookieSettings
            -> JWTSettings
            -> Credentials -- my type storing user's login and pass
            -> Handler (Headers '[Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie] NoContent)
checkCreds cookieSettings jwtSettings Credentials { credentialsUserName = userName, credentialsPassword = pass} = do
    authRes <- checkIfUserExists userName pass 
    case authRes of
        Just (name, key) -> do
            mApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings (Session key name)
            return $
                case mApplyCookies of
                    Nothing           -> clearSession cookieSettings NoContent
                    Just applyCookies -> applyCookies NoContent
        Nothing ->
            throwError err401

getLogout :: CookieSettings
            -> Handler (Headers '[ Header "Set-Cookie" SetCookie, Header "Set-Cookie" SetCookie ] NoContent)
getLogout cookieSettings = return $ clearSession cookieSettings NoContent

The cookieSettings I use are here:

cookieSettings :: CookieSettings
cookieSettings = defaultCookieSettings {
    cookieIsSecure = NotSecure,
    cookieSameSite = SameSiteStrict,
    sessionCookieName = "MyCookie",
    cookieXsrfSetting = Just def {xsrfExcludeGet = True}
}

Client

I use JavaScript fetch to poke the login endpoint:


    let opts: RequestInit = {
      method: "POST",
      headers: new Headers({ "Content-Type": "application/json" }),
      credentials: "include",
      body: json,
    };

    fetch("http://localhost:8081/login", opts)
      .then((value) => {
         // Do something
       }
     );

This works fine and I noticed the cookies are included in the response and I can find them in Storage -> Cookies in my Firefox.

Then I use a similar method to poke the logoutendpoint:

const sendLogOut = async () => {
  let resp = await fetch(Urls.logout.href, { method: "GET" });
  console.log(resp.status);
};

it prints 200 in my console and I can see the cookies are included in the response: Cookies

However, nothing else happens. It seems that the response gets discarded and the cookies I had received from login are still valid.

Questions

1.) How shall I implement the logout feature properly. 2.) As I'm relatively new to web development, where can I find useful information about HTTP protocol? By "useful" I meant "something that shows examples instead of raw definitions".


Solution

  • I've got the answer.

    In my case the problem was caused by the fact that my servant app and my JS client were technically two separate applications, hosted on two different ports (8081 and 8833 respectively).

    When I set up nginx and configured routing under a single domain, everything works as expected.