One does not simply delete cookies
Tags: webdev, http, astro
Published: 2024-06-10 10:08:31

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](https://astro.build/), to which I've added basic authentication via Twitch so users can [log in to view their inventory for my new stream game](https://p4nth3rworld.netlify.app/) 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](https://github.com/nowaythatworked/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](https://docs.astro.build/en/reference/api-reference/#astrocookies).

This code worked great in development, but it **_didn't work in production_**.

```javascript
// 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](https://www.w3schools.com/js/js_cookies.asp) 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.

```text
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](https://en.wikipedia.org/wiki/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 tools.ietf.org/html/draft-west-cookie-prefixes-05.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kwnemeh529zwb3k8id2f.png)

_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](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](https://datatracker.ietf.org/doc/html/draft-west-cookie-prefixes-05#section-3).

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.

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#httponly) prevents client-side JavaScript from accessing this cookie, for example via `document.cookie` to mitigate attacks against cross-site scripting.

[secure: true](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure) 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: "/"](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value) 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](https://github.com/withastro/docs/pull/8476) to clarify what was actually happening..