Cookies in Purescript

Hey everyone,

I am very new to frontend development, putting together a Halogen website (with a Haskell backend). However, I am experiencing a lot of issues with internet security and authentication, particularly with how to deal with cookies through Javascript. It seems like much of what is being communicated between the backend and frontend is purposely kept hidden from the Javascript code, to avoid security issues in javascript (cross-site scripting and such).

So I was wondering, given a backend that implements authentication by sending JWT in a “Set-Cookie” header, what would be the Purescript/Javascript industry standard for dealing with this authentication? Should I change the backend to work with some other system?

On a more general note, how does one deal with this apparent push away from pure-javascript-websites related to internet security and other reasons? Is it common to write essentially a separate Purescript “app” for each significantly separate path?

I’m aware this isn’t as much a Purescript-specific issue as it is a Javascript-issue, but it seemed worth asking :slight_smile:

3 Likes

After receiving a response with a Set-Cookie header, the browser should generally include cookies in subsequent requests without you having to ask it to. When making a cross-origin request (e.g. if your API server and frontend live on separate domains) you might need to ask the browser to include cookies, e.g. by using the {credentials: "include"} option if you’re using fetch. You’re right that it’s ideal to have the actual cookie value inaccessible to your JS code, because if your JS code has access to the cookie value, then it might be easier for attackers to trick people into leaking their authentication cookies (which would allow them to impersonate other users).

Can you elaborate on the issues you are experiencing? Do you need access to the JWT in PureScript so that you can parse it and use that information in your frontend?

3 Likes

The specific issue I’m experiencing, is that I seem unable to find any way to actually access the “Set-Cookie” response header through the Purescript code. A few important things to mention:

  • I’m currently building a javascript-only app. By this I mean that the HTML I wrote myself is literally just starting up the app, and everything else is handled within Purescript.
  • I am using Ajax (Affjax) to create and send the requests (and process the responses) used to converse with the backend
  • The backend and frontend are on the same domain name. Even in development (where everything is just on localhost), I’m having these issues.
  • I turned off HttpOnly and Secure, for the time being, until this is debugged.

Concretely, I set up a “Login” Affjax request, send it, unpack the response and print the headers (to console, as a way to debug things), and the only header that is being shown in that way, is the “content-type” one. If I check my network, I can see the response to the “Login” request does contain a “Set-Cookie” header though. And that header is not being used to create a Cookie and put it into my browser.

From what I found online, it seems like Ajax blocks cookie-setting (and using) headers to prevent security vulnerabilities, but I haven’t been able to figure out yet what people use instead.

1 Like

Yes, the fetch specification defines Set-Cookie as a forbidden response header, which means that it is not accessible via the response object. However, if a cookie does not have the HttpOnly flag set, then it should be accessible via document.cookie. Although, you should note that this will only work if you leave HttpOnly turned off permanently.

If you can control what the server does, I would strongly recommend avoiding JWT for authentication. As I say, having the actual cookie value accessible to JS is a big security risk, and it’s usually not necessary. My advice would be to use an opaque token (i.e. not one that includes any form of structured data) in a cookie for authentication, and then if you need to give the client any additional information about the current user or session, to provide that via separate API endpoints.

3 Likes

I typed most of this up the other night on the tablet while falling asleep in bed, but didn’t hit submit it as I figured it was full of typos. And then forgot about it the next day. :sweat_smile: I appear to have gotten a little carried away (…again), but I hope it helps.


Huge caveat that I’m not a SEng professional and mostly have no idea what I’m doing. So I definitely can’t speak to current best practices.

But…

Here’s the model I use (with a PureScript Halogen front-end and Haskell Servant backend) that keeps the JWT (…with its structured data about the logged-in user) in a Secure, HttpOnly cookie (…and so inaccessible to JS).

On loading of your client app’s main function, send an Affjax request for the profile of the current user to a JWT-protected route on your backend. Basically it’s a getCurrentUser function on the client.

If the browser already has the HttpOnly cookie set when the request is sent, then the browser will automatically send this cookie along with that request to the backend.

If the backend determines that the cookie contained a [currently] valid JWT, the backend should send back the current user profile as the JSON response to the request. Your client can then store this in your app’s state (however you are managing that) and render the pages as that logged-in user. Your backend could get the profile by:

  1. storing a userId in the JWT and using that to look that up the user’s profile in the DB (though having to send that query loses one of a JWT’s main advantages)
  2. skipping the DB query by storing the entire user profile in the JWT and responding with that (if the JWT is validated).
    • So the idea is not that the PureScript Halogen client reads the JWT to get the Profile to display
    • …but rather that the Halogen client does nothing as the browser sends the JWT-containing-cookie with the request to the Haskell server, the Haskell server validates the JWT, and sends back the Profile contents as a JSON response.

You mentioned a Haskell backend. I’ll take a wild guess and say maybe servant-auth-server? If so, the contents of the validated HttpOnly JWT is available as an argument to all of the handlers for your routes requiring that authentication. Your handler for the getCurrentUser would basically just be that argument applied to pure or return.

If a JWT was sent in a HttpOnly cookie that’s not valid (…or no longer valid), the backend can send back an invalid auth response (with no user profile). You can have your client render the page in whatever way is appropriate for no profile being stored. Maybe including a redirect to the login page if they were trying to load a protected page directly without having valid credentials saved.

If I continue with my wild guess that you’re using servant-auth-server on the backend (correct me if I’m wrong), there’s a clearSession function there that you can use to have your invalid auth response from the sever also send new blank Set-Cookies. This will overwrite the existing invalid JWT stored by that client’s browser.

But yeah nowhere along the way here in this example have we been handling/parsing/validating the JWT in the Halogen client app. That’s all handled by the backend that’s receiving the [Secure] HttpOnly cookies the browser automatically sends with its requests and then sending back the protected data if the JWT was valid (…or sending a response code / error that tells the client they need to log in or are not permitted to access a given resource, etc.).

If you did want the practice of reading a token from cookies and sending it in the request headers from your client, you could do that with an (additional) optional XSRF token. This is again something that servant-auth-server allows for as one of its defaults. This token will just be a random nonce and not contain any user data.

So if you go down this path then on authenticated requests, your backend would send both:

  • Some version of a user profile in a JWT stored in a Set-Cookie (both Secure and HttpOnly, not accessible to JS)
  • A random string of bytes in a Set-Cookie that does not have HttpOnly set (…and so is accessible to JS)

The random bytes are sent from server to client as the value for a XSRF-TOKEN key in the JS-accessible Set-Cookie. And then your client sends it back with the request in a similarly-named-but-not-exactly-the-same X-XSRF-TOKEN Affjax request header (note the extra X- prefix that seems to often catch people).

The browser will automatically send this non-HttpOnly cookie with the XSRF-TOKEN key/value (…just like it will also be doing with the HttpOnly cookie containing the JWT). As Harry mentioned, to get that non-HttpOnly cookie you can FFI in PureScript to document.cookie. And then use the biscotti-cookie PureScript library to parse out the cookie’s value (just a random string of bytes that the backend sent).

And then your backend would only consider your client’s requests to it as valid if now both the:

  • JWT in the HttpOnly cookie it received was validated as okay; AND
  • the value of the request header you sent for the X-XSRF-TOKEN key also matches the random value of the XSRF-TOKEN key in the cookie (that, again, the browser sent for you). servant-auth-server's plumbing checks that these two values match for you.

And the only time your Halogen client touches the cookies was before a request is sent to read the currently-stored value of the XSRF token in your document.

Not going to lie. I struggled a bit at the start (…and for a fair time after) to work out how to adapt the authentication model shown in the purescript-halogen-realworld project (…where, per the GoThinkster RealWorld spec, the JWT token is returned to the client as part of a regular JSON response along with the profile) to something that worked with a backend that only sent and validated JWTs stored in a Secure / HttpOnly cookie.

It was pretty satisfying to get working in the end.

Happy to answer questions or take criticism of anything about this method :slight_smile:

e.g. are XSRF tokens necessary if you are SameSite Strict-ing? or is including them as defence-in-depth okay? I don’t know!

2 Likes

The problem seems to lie in that my browser doesn’t automatically send back the cookie after receiving a “Set-Cookie” response header. But the example does help, thanks! I’m sure I’ll be able to debug the issue now :slight_smile:

P.S.: Your (educated) guesses were all correct!

2 Likes