Skip to main content
whitep4nth3r logo

One does not simply delete cookies

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

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 indicates 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.

Like weird newsletters?

Join 338+ subscribers in the Weird Wide Web Hole to find no answers to questions you didn't know you had.

Subscribe

Salma is looking at you, with a rather large smile. She's pointing across herself up to her left, with a very tatooed arm. She's wearing a black shirt and black rimmed glasses.

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.

Related posts

28 Sep 2023

How to use jQuery with Astro

Understanding how to use jQuery in an Astro project was hard to Google. So I wrote my own guide for you and my future self.

Web Dev 2 min read →

24 Apr 2024

Why don’t we talk about minifying CSS anymore?

Remember Grunt files?

CSS 8 min read →