Sometimes, even the most "intuitive" framework APIs can create misunderstandings in the minds of seasoned web developers (ahem, me).
Naming is hard. Modern developer tools often provide intuitive APIs acting as wrappers around web platform APIs to make our lives easier and developer experience smoother. But sometimes, these "intuitive" APIs can create misunderstandings in the minds of even the most seasoned web developers (ahem, me).
I'm currently building a project using Astro, to which I've added basic authentication via Twitch so users can log in to view their inventory for my new stream game by calling an API on the back end (my Twitch bot). I'm using Astro in SSR mode, and authentication is provided by Auth.js via auth-astro. When using Auth.js to authenticate, it saves three cookies in the browser to remember that you've logged in to this website via Twitch.
At the time of writing, auth-astro doesn't provide sign out functionality for Astro on the server. However, I needed some sign out functionality on the server to provide a better front end experience when the Twitch accessToken
was identified as invalid by my back end. I created a logout route to "delete" the authentication cookies provided by Auth.js using Astro's Astro.cookies API.
This code worked great in development, but it didn't work in production.
// src/pages/logout.astro
// DEV
Astro.cookies.delete("authjs.csrf-token");
Astro.cookies.delete("authjs.callback-url");
Astro.cookies.delete("authjs.session-token");
// PROD
Astro.cookies.delete("__Host-authjs.csrf-token");
Astro.cookies.delete("__Secure-authjs.callback-url");
Astro.cookies.delete("__Secure-authjs.session-token");
return Astro.redirect("/");
The cookie-savvy among you can probably already tell what I was doing wrong. But after deploying my project to multiple hosting platforms to check it wasn't a platform-specific problem with Astro, I learned a few things about how cookies are marked as "deleted", and how servers send instructions to browsers to create and modify cookies.
Cookies are not deleted: they're modified
As a front end developer, I am accustomed to managing cookies via client-side JavaScript using the document.cookie API. There is no way to "delete" a cookie using client-side JavaScript, you modify it: "deleting" a cookie is a misnomer. Whilst the word "delete" feels intuitive in the Astro.cookies
API, it hides the fact that to "delete" a cookie, you need to invalidate it by setting an expiry date in the past.
Additionally, you're not technically modifying a browser cookie directly via server-side code. So what's actually happening?
Cookies are modified via HTTP response headers
After receiving an HTTP request, a server can send cookie modification requests to a browser via one or more Set-Cookie
HTTP response headers. For example, when you call Astro.cookies.delete("my_cookie")
, you'll see the following response header in the browser network tab.
Set-Cookie: my_cookie=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT
This instructs the browser to store the my_cookie
value as deleted
, and sets the expiration date to a date in the past: 0 in Unix time. It's worth bearing in mind that expiring a cookie doesn't necessarily remove it from the browser storage in all browsers, but browsers will not send expired cookies to the server in subsequent HTTP requests. (Expired cookies also won't show up in the Application tab in browser dev tools.)
Why weren't my cookies being removed in production?
In the code example above, notice that the cookie names for development and production are different. The production cookies are prefixed by __Secure-
and __Host-
. When inspecting the HTTP headers sent on the logout page in production, I noticed this warning.
This attempt to set a cookie via a Set-Cookie header was blocked because it used the "_Secure-" or "_Host-" prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in https://tools.ietf.org/html/draft-west-cookie-prefixes-05.
If you click the link to this document, you'll see that it's a draft which expired in 2016. For this reason I didn't feel inclined to read it. Why was my browser showing me this in 2024? Regardless, I did a search on the page for __Secure-
and found this section on cookie prefixes.
The short version of this story is that the browser was rejecting my Set-Cookie
HTTP response headers that were requesting to modify cookies. This was because the cookie options were not specified correctly: they didn't match the cookie options that were specified on the existing cookies. By checking the cookie options in the Application tab in browser dev tools, and updating the code accordingly, I was successfully able to modify the expiration date of the specified cookies, and effectively log the user out of my application.
Astro.cookies.delete("__Host-authjs.csrf-token", {httpOnly: true, secure: true, path: "/"});
Astro.cookies.delete("__Secure-authjs.callback-url", {httpOnly: true, secure: true});
Astro.cookies.delete("__Secure-authjs.session-token", {httpOnly: true, secure: true});
What do the options mean?
httpOnly: true
prevents client-side JavaScript from accessing this cookie, for example via document.cookie
to mitigate attacks against cross-site scripting.
secure: true
i
ndicates that the cookie is sent to the server only when a request is made with https:
(except on localhost), making it more resistant to man-in-the-middle attacks.
path: "/"
indicates the path that must exist in the requested URL for the browser to send the Set-Cookie
header. This was required given I was redirecting to "/" straight after setting the cookies.
The reason that the initial code successfully modified the cookies in development was because the cookie names were not prefixed with __Host-
or __Secure-
, and because I was running the site on localhost
.
Naming is hard
I'm a big fan of naming things after their specific function. Should the Astro.cookies
API rename delete
to modify
in order to be more explicit? Probably not. In any case, I decided to open a PR to the Astro docs to clarify what was actually happening.
Salma Alam-Naylor
I'm a live streamer, software engineer, and developer educator. I help developers build cool stuff with blog posts, videos, live coding and open source projects.