A practical, Complete Tutorial on HTTP cookies
Tags: beginners, http, javascript, python
Published: 2020-09-16 06:41:35

---
title: "A practical, Complete Tutorial on HTTP cookies"
published: true
description: "Learn how HTTP cookies work: simple, practical examples with JavaScript and Python."
tags: beginners, http, javascript, python
canonical_url: https://www.valentinog.com/blog/cookies/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/19ji7u6v2qgn0t4krye4.png
---

## What are cookies in web development?

**Cookies are tiny pieces of data that the backend can store in the user's browsers**. User tracking, personalization, and most important, **authentication**, are the most common use cases for cookies.

Cookies have a lot of privacy concerns, and have been subject to strict regulation over the years. 

**In this post I'll focus mainly on the technical side: you'll learn how to create, use, and work with HTTP cookies**, on the frontend, and on the backend.

## What you will learn

In the following guide you'll learn:

- how to work with cookies, backend and frontend
- cookie **security and permissions**
- interaction between **cookies, AJAX, and CORS**

## Setting up the backend

The examples for the **backend** are in **Python with Flask**. If you want to follow along, create a new Python virtual environment, move into it, and install Flask:

```bash
mkdir cookies && cd $_

python3 -m venv venv
source venv/bin/activate

pip install Flask
```

In the project folder create a new file named `flask_app.py`, and use my examples to experiment locally.

## Who creates cookies?

First things first, **where does cookies come from? Who creates cookies?**

While it's possible to create cookies in the browser with `document.cookie`, most of the times it's responsibility of the **backend to set cookies in the response before sending it to the client**. 

By **backend here we mean** that cookies can be created by:

- the actual application's code on the backend (Python, JavaScript, PHP, Java)
- a webserver responding to requests (Nginx, Apache)

For doing so the backend sets in the response an HTTP header named `Set-Cookie` with a corresponding string made of a key/value pair, plus optional attributes:

```text
Set-Cookie: myfirstcookie=somecookievalue
```

When and where to create these cookies depends on the requirements.

So, **cookies** are simple strings. Consider this example in Python with Flask. Create a Python file named `flask_app.py` in the project folder with the following code:

```python
from flask import Flask, make_response

app = Flask(__name__)


@app.route("/index/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
    return response
```

Then run the app:

```bash
FLASK_ENV=development FLASK_APP=flask_app.py flask run
```

When this application is running, and the user visits http://127.0.0.1:5000/index/ the backend sets a **response header** named `Set-Cookie` with a key/value pair. 

(127.0.0.1:5000 is the default listening address/port for Flask applications in development).

The `Set-Cookie` header is the key to understand how to create cookies:

```python
response.headers["Set-Cookie"] = "myfirstcookie=somecookievalue"
```
 
On the right side you can see the actual cookie `"myfirstcookie=somecookievalue"`. 

Most frameworks have their own utility functions for setting cookies programmatically, like [Flask's](https://flask.palletsprojects.com/en/1.1.x/api/?highlight=set_cookie#flask.Response.set_cookie) `set_cookie()`. 

Under the hood they simply set a header in the response with `Set-Cookie`.

## How to see cookies?

Consider again the previous example with Flask. Once you visit http://127.0.0.1:5000/index/, the backend sets a cookie in the browser. To see this cookie you can either call `document.cookie` from the browser's console:

![document.cookie](https://dev-to-uploads.s3.amazonaws.com/i/tfm0p7ep7o6ozing3ajc.png)

Or you can check the **Storage** tab in the developer tools. Click on **Cookies**, and you should see the cookie there:

![Cookie storage](https://dev-to-uploads.s3.amazonaws.com/i/0ndg6hai82vpxf8mubjs.png)

On a command line you can use also **curl** to see what cookies the backend sets:

```bash
curl -I http://127.0.0.1:5000/index/
``` 

To save cookies to a file for later use:

```bash
curl -I http://127.0.0.1:5000/index/ --cookie-jar mycookies
``` 

To display cookies on stdout:

```bash
curl -I http://127.0.0.1:5000/index/ --cookie-jar -
``` 

Note that cookies without the `HttpOnly` attribute are accessible on `document.cookie` from JavaScript in the browser. On the other hand a cookie marked as `HttpOnly` cannot be accessed from JavaScript.

To mark a cookie as `HttpOnly` pass the attribute in the cookie:

```text
Set-Cookie: myfirstcookie=somecookievalue; HttpOnly
```

Now the cookie will still appear in the Cookie Storage tab, but `document.cookie` will return an empty string.

From **this point on for convenience I'll use Flask's** `response.set_cookie()` **to create cookies on the backend**.

To inspect cookies along the way in this guide we'll use alternatively:

- curl
- Firefox developer tools
- Chrome developer tools

## I've got a cookie, what now?

Your browser gets a cookie. Now what? Once you have a cookie, the browser **can send back the cookie to the backend**.

This could have a number of applications: user tracking, personalization, and most important, **authentication**.

For example, once you log in in a website the backend can give you a cookie:

```text
Set-Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
```

To **properly identify you on each subsequent request, the backend checks the cookie coming from the browser in the request**.

To send the cookie, the browser appends a `Cookie` header in the request:

```text
Cookie: userid=sup3r4n0m-us3r-1d3nt1f13r
```

**How, when, and why the browser sends back cookies** is the topic for the next sections.

## Cookies can expire: Max-Age and expires

By default, **cookies expire when the user closes the session, that is, when she closes the browser**. To persist a cookie we can pass `expires` or `Max-Age` attributes:

```text
Set-Cookie: myfirstcookie=somecookievalue; expires=Tue, 09 Jun 2020 15:46:52 GMT; Max-Age=1209600
```

When bot attributes are present, `Max-Age` has precedence over `expires`.

## Cookies are scoped by path: the Path attribute

Consider this backend which sets a new cookie for its frontend when visiting http://127.0.0.1:5000/. On the other two routes instead we print the request's cookies:

```python
from flask import Flask, make_response, request

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d", path="/about/")
    return response


@app.route("/about/", methods=["GET"])
def about():
    print(request.cookies)
    return "Hello world!"


@app.route("/contact/", methods=["GET"])
def contact():
    print(request.cookies)
    return "Hello world!"
```

To run the app:

```bash
FLASK_ENV=development FLASK_APP=flask_app.py flask run
```

In another terminal, if we make connection with the root route we can see the cookie in `Set-Cookie`:

```bash
curl -I http://127.0.0.1:5000/ --cookie-jar cookies

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 23
Set-Cookie: id=3db4adj3d; Path=/about/
Server: Werkzeug/1.0.1 Python/3.8.3
Date: Wed, 27 May 2020 09:21:37 GMT
```

Notice how the cookies has a `Path` attribute:

```text
Set-Cookie: id=3db4adj3d; Path=/about/
```

Let's now visit the /about/ route by sending the cookie we saved in the first visit:

```bash
curl -I http://127.0.0.1:5000/about/ --cookie cookies
```

In the terminal where the Flask app is running you should see:

```text
ImmutableMultiDict([('id', '3db4adj3d')])
127.0.0.1 - - [27/May/2020 11:27:55] "HEAD /about/ HTTP/1.1" 200 -
```

As expected the cookie goes back to the backend. Now try to visit the /contact/ route:

```bash
curl -I http://127.0.0.1:5000/contact/ --cookie cookies
```

This time in the terminal where the Flask app is running you should see:

```text
ImmutableMultiDict([])
127.0.0.1 - - [27/May/2020 11:29:00] "HEAD /contact/ HTTP/1.1" 200 -
```

What that means? Cookies are scoped by path. **A cookie with a given** `Path` **attribute cannot be sent to another, unrelated path, even if both path live on the same domain**.

This is the first layer of **permissions** for cookies.

When `Path` is omitted during cookie creation, the browsers defaults to **/**.

## Cookies are scoped by domain: the Domain attribute

The value for the `Domain` attribute of a cookie controls **whether the browser should accept it or not** and **where the cookie goes back**.

Let's see some examples.

**NOTE**: the following URL are on free Heroku instances. Give it a second to spin up. Open up a browser's console before opening the links to see the result in the network tab.

### Non matching host (wrong host)

Consider the following cookie set by https://serene-bastion-01422.herokuapp.com/get-wrong-domain-cookie/:

```text
Set-Cookie: coookiename=wr0ng-d0m41n-c00k13; Domain=api.valentinog.com
```

Here the cookie originates from **serene-bastion-01422.herokuapp.com**, but the `Domain` attribute has **api.valentinog.com**.

There's no other choice for the browser to **reject this cookie**. Chrome for example gives a warning (Firefox does not):

![Browser blocks wrong domain in cookie](https://dev-to-uploads.s3.amazonaws.com/i/qxvx09xzrnia4ddpf0im.png)

### Non matching host (subdomain)

Consider the following cookie set by https://serene-bastion-01422.herokuapp.com/get-wrong-subdomain-cookie/:

```text
Set-Cookie: coookiename=wr0ng-subd0m41n-c00k13; Domain=secure-brushlands-44802.herokuapp.com
```

Here the cookie originates from **serene-bastion-01422.herokuapp.com**, but the `Domain` attribute is **secure-brushlands-44802.herokuapp.com**.

They are on the same domain, but the subdomain is different. Again, the browser rejects this cookie as well:

![Browser blocks wrong domain in cookie](https://dev-to-uploads.s3.amazonaws.com/i/em10zttg322jd87njltm.png)

### Matching host (whole domain)

Consider now the following cookie set by visiting https://www.valentinog.com/get-domain-cookie.html:

```text
set-cookie: cookiename=d0m41n-c00k13; Domain=valentinog.com
```

This cookie is set at the web server level with [Nginx add_header](http://nginx.org/en/docs/http/ngx_http_headers_module.html):

```bash
add_header Set-Cookie "cookiename=d0m41n-c00k13; Domain=valentinog.com";
```

I **used Nginx here to show you there are various ways to set a cookie**. The fact that a cookie is set by a web server or by the application's code **doesn't matter much for the browser**.

What matters is the domain the cookie is coming from.

Here the browser will **happily accept the cookie** because the host in `Domain` **includes the host from which the cookie came**.

In other words, valentinog.com includes the subdomain www.valentinog.com.

Also, the **cookie travels back with any new request against valentinog.com**, as well as **any request to subdomains on valentinog.com**.

Here's a request to the www subdomain with the cookie attached:

![Cookie sent back matching domain](https://dev-to-uploads.s3.amazonaws.com/i/w2k2lq0ei087jzyuo9e7.png)

Here's a request to another subdomain with the cookie automatically attached:

![Cookie subdomain sent back matching domain](https://dev-to-uploads.s3.amazonaws.com/i/z7j15ck1dm8c9yvbq38d.png)

### Cookies and the Public Suffix List

Now consider the following cookie set by https://serene-bastion-01422.herokuapp.com/get-domain-cookie/:

```text
Set-Cookie: coookiename=d0m41n-c00k13; Domain=herokuapp.com
```

Here the cookie comes from **serene-bastion-01422.herokuapp.com**, and the `Domain` attribute is **herokuapp.com**. What should the browser do here?

You might think that serene-bastion-01422.herokuapp.com is included in the domain herokuapp.com, so the browser should accept the cookie.

Instead, **it rejects the cookie** because it comes from a domain included in the **Public Suffix List**. 

The **Public Suffix List** is a list maintained by Mozilla, used by all browsers to restrict who can set cookies on behalf of other domains.

Resources:

- [Public suffix list](https://publicsuffix.org/)
- [Cookies and the Public Suffix List](https://devcenter.heroku.com/articles/cookies-and-herokuapp-com)

### Matching host (subdomain)

Consider now the following cookie set by https://serene-bastion-01422.herokuapp.com/get-subdomain-cookie/:

```text
Set-Cookie: coookiename=subd0m41n-c00k13
```

When `Domain` is omitted during cookie creation, the browsers defaults to the originating host in the address bar, in this case my code does:

```python
response.set_cookie(key="coookiename", value="subd0m41n-c00k13")
```

When the cookie lands in the browser's cookie storage we see the `Domain` applied:

![Default domain attribute](https://dev-to-uploads.s3.amazonaws.com/i/oe4t554xikygqraq2nuj.png)

So we have this cookie from serene-bastion-01422.herokuapp.com. **Where this cookie should be sent now?**.

If you visit https://serene-bastion-01422.herokuapp.com/ the cookie goes with the request:

![Cookie subdomain sent](https://dev-to-uploads.s3.amazonaws.com/i/j1ipy6v9igegb95ib4w1.png)

But, if you visit herokuapp.com **the cookie does not leave the browser at all**:

![Cookie from subdomain to domain not sent](https://dev-to-uploads.s3.amazonaws.com/i/cml6d6ija9cw0if8jvn9.png)

(It doesn't matter that herokuapp.com later redirects to heroku.com).

To recap, the browser uses the following heuristics to decide what to do with cookies (by sender host here I mean the actual URL you visit):

- **Reject the cookie** altogether if either the domain or the subdomain in `Domain` don't match the sender host
- **Reject the cookie** if the value of `Domain` is included in the Public suffix list
- **Accept the cookie** if the domain or the subdomain in `Domain` matches the sender host

Once the browsers accepts the cookie, and it's about to **make a request** it says:

- **Send it back the cookie** if the request host matches exactly the value I saw in `Domain`
- **Send it back the cookie** if the request host is a subdomain matching exactly the value I saw in `Domain`
- **Send it back the cookie** if the request host is a subdomain like sub.example.dev included in a `Domain` like example.dev
- **Don't send it back the cookie** if the request host is a main domain like example.dev and `Domain` was sub.example.dev

**Takeaway**: `Domain` is the second layer of **permissions** for cookies, alongside with the `Path` attribute.

## Cookies can travel over AJAX requests

Cookies can travel over AJAX requests. **AJAX requests** are asynchronous HTTP requests made with JavaScript (XMLHttpRequest or Fetch) to get and send back data to a backend.

Consider another example with Flask where we have a template, which in turn loads a JavaScript file. Here's the Flask app:

```python
from flask import Flask, make_response, render_template

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response
```

Here's the template in `templates/index.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</html>
```

Here's the JavaScript code in `static/index.js`:

```javascript
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie();
});

function getACookie() {
  fetch("/get-cookie/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.text();
    })
    .then(text => console.log(text));
}
```

When visiting http://127.0.0.1:5000/ we see a button. By clicking the button we make a Fetch request to /get-cookie/ to obtain a cookie back. As expected the cookie lands in the browser's Cookie storage.

Now let's change a bit our Flask app to expose another endpoint:

```python
from flask import Flask, make_response, request, render_template, jsonify

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")
```

Also, let's tweak our JavaScript code so that we make another Fetch request after getting the cookie:

```javascript
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}
```

When visiting http://127.0.0.1:5000/ we see a button. By clicking the button we make a Fetch request to /get-cookie/ to obtain a cookie back. As soon as the cookie comes, we make another Fetch request to /api/cities/.

In the browser's console you should see an array of cities. Also, in the Network tab of the developer tool you should see a header named `Cookie`, transmitted to the backend over the AJAX request:

![Fetch API cookie header](https://dev-to-uploads.s3.amazonaws.com/i/cbnspnys2rnrvg08p993.png)

This **cookie exchange back and forth between frontend and backend works fine as long as the frontend is in the same context of the backend**: we say that they're on the same origin.

That's because by default, Fetch sends **credentials, i.e. cookies** only when the request hits the same origin from which the request fires.

Here, JavaScript is served by a Flask template on http://127.0.0.1:5000/.

Let's see instead what happens for different origins.

## Cookies cannot always travel over AJAX requests

Consider a different situation where the backend runs stand-alone, so you have this Flask app running:

```bash
FLASK_ENV=development FLASK_APP=flask_app.py flask run
```

Now in a different folder, outside of the Flask app, create an `index.html`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<button>FETCH</button>
</body>
<script src="index.js"></script>
</html>
```

Create in the same folder a JavaScript file named `index.js` with the following code:

```javascript
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/").then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/")
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}
```

In the same folder, from the terminal run:

```bash
npx serve
```

This command gives you a local address/port to connect to, like `http://localhost:42091/`. Visit the page and try to click the button with the browser's console open. In the console you should see:

```text
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)
```

Now, `http://localhost:5000/` is not the same as `http://localhost:42091/`. They're different **origins**, hence **CORS** kick ins.

An **origin consists of a scheme, domain, and port number**. That means `http://localhost:5000/` is a different origin from `http://localhost:42091/`.

## Dealing with CORS

CORS, acronym for Cross-Origin Resource Sharing, is a way for servers to control access to resources on a given origin, when JavaScript code running on a different origin requests these resources. 

By default, browsers block AJAX requests to remote resources which are not on the same origin, unless a specific HTTP header named `Access-Control-Allow-Origin` is exposed by the server.

To fix this first error we need to configure CORS for Flask:

```bash
pip install flask-cors
```

Then apply CORS to Flask:

```python
from flask import Flask, make_response, request, render_template, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app=app)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/get-cookie/", methods=["GET"])
def get_cookie():
    response = make_response("Here, take some cookie!")
    response.set_cookie(key="id", value="3db4adj3d")
    return response


@app.route("/api/cities/", methods=["GET"])
def cities():
    if request.cookies["id"] == "3db4adj3d":
        cities = [{"name": "Rome", "id": 1}, {"name": "Siena", "id": 2}]
        return jsonify(cities)
    return jsonify(msg="Ops!")
```

Now try to click again the button with the browser's console open. In the console you should see:

```text
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/api/cities/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing)
```

Despite we got the same error, this time the culprit lies in the second route.
 
**There's no such cookie named "id" attached to the request**, so Flask crashes and no `Access-Control-Allow-Origin` gets set.

You can confirm this by looking at the request in the Network tab. No such `Cookie` is sent:

![Fetch no cookies attached](https://dev-to-uploads.s3.amazonaws.com/i/100ivrq1r1byjcfy812r.png)

**To include cookies in a Fetch requests across different origins we must provide the** `credentials` **flag (by default it's same origin).** 

**Without this flag Fetch simply ignores cookies**. To fix our example:

```javascript
const button = document.getElementsByTagName("button")[0];

button.addEventListener("click", function() {
  getACookie().then(() => getData());
});

function getACookie() {
  return fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  }).then(response => {
    // make sure to check response.ok in the real world!
    return Promise.resolve("All good, fetch the data");
  });
}

function getData() {
  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })
    .then(response => {
      // make sure to check response.ok in the real world!
      return response.json();
    })
    .then(json => console.log(json));
}
```

`credentials: "include"` has to be present on the first Fetch request, to save the cookie in the browser's Cookie storage:

```javascript
fetch("http://localhost:5000/get-cookie/", {
    credentials: "include"
  })
```

It has also to be present on the second request to allow transmitting cookies back to the backend:

```javascript
  fetch("http://localhost:5000/api/cities/", {
    credentials: "include"
  })
```

Try again, and you'll see we need to fix another error on the backend:

```text
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5000/get-cookie/. (Reason: expected ‘true’ in CORS header ‘Access-Control-Allow-Credentials’).
```

To **allow cookie transmission** in CORS requests, the backend needs to expose the `Access-Control-Allow-Credentials` header as well. Easy fix:

```python
CORS(app=app, supports_credentials=True)
```

Now you should see the expected array of cities in the browser's console.

**Takeaways**: to make cookies travel over AJAX requests between different origins provide:
 
- `credentials: "include"` on the frontend for Fetch 
- `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` on the backend.

**Cookies can travel over AJAX requests, but they have to respect the domain rules we described earlier**.

Resources:

- [Fetch request credentials](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
- [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
- [Cross-origin fetches](https://www.youtube.com/watch?v=vfAHa5GBLio)

## A concrete example

Our previous example uses localhost to keep things simple and replicable on your local machine.

To **imagine cookie exchange over AJAX requests in the real world** you can think of the following scenario:

1. a user visits https://www.a-example.dev
2. she clicks a button or makes some action which triggers a Fetch request to https://api.b-example.dev
3. https://api.b-example.dev sets a cookie with `Domain=api.b-example.dev`
4. on subsequent Fetch requests to https://api.b-example.dev the cookie is sent back

## Cookies can be kind of secret: the Secure attribute

**But not so secret after all.**

The `Secure` attribute for a cookie ensures that the cookie **is never accepted over HTTP**, that is, **the browser rejects secure cookies unless the connection happens over HTTPS**.

To mark a cookie as `Secure` pass the attribute in the cookie:

```text
Set-Cookie: "id=3db4adj3d; Secure"
```

In Flask:

```python
response.set_cookie(key="id", value="3db4adj3d", secure=True)
```

If you want to try against a live environment, run the following command on the console and note how curl here **does not save the cookie over HTTP**:

```bash
curl -I http://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
```

**Note**: this will work only in curl 7.64.0 >= which implements rfc6265bis. Older versions of curl implement RCF6265. [See](https://github.com/curl/curl/issues/5473)

Over HTTPS instead, the cookie appears in the cookie jar:

```bash
curl -I https://serene-bastion-01422.herokuapp.com/get-secure-cookie/ --cookie-jar -
```

Here's the jar:

```text
serene-bastion-01422.herokuapp.com      FALSE   /       TRUE    0       id      3db4adj3d
```

To try the cookie in a browser visit both versions of the url above and check out the Cookie storage in the developer tool.

Don't get fooled by `Secure`: browsers accept the cookie over **HTTPS**, but there's no protection for the cookie once it lands in the browser.

For this reason a `Secure` **cookie, like any cookie, is not intended for transmission of sensitive data**, even if the name would suggest the opposite. 

## Don't touch my cookie: the HttpOnly attribute

The `HttpOnly` attribute for a cookie ensures that the cookie **is not accessible by JavaScript code**. This is the most **important form of protection against XSS attacks**

However, it is **sent on each subsequent HTTP request**, with respect of any permission enforced by `Domain` and `Path`.

To mark a cookie as `HttpOnly` pass the attribute in the cookie:

```text
Set-Cookie: "id=3db4adj3d; HttpOnly"
```

In Flask:

```python
response.set_cookie(key="id", value="3db4adj3d", httponly=True)
```

A cookie marked as `HttpOnly` cannot be accessed from JavaScript: if inspected in the console, `document.cookie` returns an empty string. 

However, **Fetch can get, and send back** `HttpOnly` cookies when `credentials` is set to `include`, again, with respect of any permission enforced by `Domain` and `Path`:

```javascript
fetch(/* url */, {
  credentials: "include"
})
```

When to use `HttpOnly`? **Whenever you can**. Cookies should always be `HttpOnly`, unless there's a specific requirement for exposing them to runtime JavaScript.

Resources:

- [What is XSS](https://owasp.org/www-community/attacks/xss/)
- [Protecting Your Cookies: HttpOnly](https://blog.codinghorror.com/protecting-your-cookies-httponly/)

## The dreaded SameSite attribute

### First and third-party cookie

Consider a cookie acquired by visiting https://serene-bastion-01422.herokuapp.com/get-cookie/:

```text
Set-Cookie: simplecookiename=c00l-c00k13; Path=/
```

We refer to this kind of cookies as **first-party**. That is, I visit that URL in the browser, and if I visit the same URL, or another path of that site (provided that `Path` is **/**) the browser sends the cookie back to the website. Normal cookie stuff.

Now consider another web page at https://serene-bastion-01422.herokuapp.com/get-frog/. This page sets a cookie as well, and in addition it **loads an image from a remote resource** hosted at https://www.valentinog.com/cookie-frog.jpg. 

This **remote resource in turns sets a cookie on its own**. You can see the actual scenario in this picture:

![Third-party-cookie](https://dev-to-uploads.s3.amazonaws.com/i/5bujf8chhir8n49e52pw.png)

**Note**: If you're on Chrome 85 you won't see this cookie. Starting from this version Chrome rejects it.

We refer to this kind of cookies as **third-party**. Another example of third-party cookie:

1. a user visits https://www.a-example.dev
2. she clicks a button or makes some action which triggers a Fetch request to https://api.b-example.dev
3. https://api.b-example.dev sets a cookie with `Domain=api.b-example.dev`
4. now the page at https://www.a-example.dev holds a **third-party** cookie from https://api.b-example.dev

### Working with SameSite

At the time of writing, third-party cookies causes a warning to pop up in the **Chrome** console:

> "A cookie associated with a cross-site resource at http://www.valentinog.com/ was set without the SameSite attribute. A future release of Chrome will only deliver cookies with cross-site requests if they are set with SameSite=None and Secure.

What the browser is trying to say is that **third-party cookies** must have the new `SameSite` attribute. But why?

The `SameSite` attribute is a **new feature aimed at improving cookie security** to: prevent **Cross Site Request Forgery attacks**, avoid privacy leaks.
 
`SameSite` can be assigned one of these three values:

- **Strict**
- **Lax**
- **None**

If we are a service providing embeddable widgets (iframes), or we need to put cookies in remote websites (for a good reason and not for wild tracking), these cookies must be marked as `SameSite=None`, and `Secure`:

```text
Set-Cookie: frogcookie=fr0g-c00k13; SameSite=None; Secure
```

Failing to do so will make the **browser reject the third-party cookie**. Here's what browsers are going to do in the near future:

> A cookie associated with a cross-site resource at http://www.valentinog.com/ was set without the SameSite attribute. It has been blocked, as Chrome now only delivers cookies with cross-site requests if they are set with SameSite=None and Secure.

In other words `SameSite=None; Secure` will make third-party cookies work as they work today, the only difference being that they must be transmitted only over HTTPS.

A cookie configured this way is sent alongside each request if domain and path matches. This is the normal behaviour.

Worth noting, `SameSite` does not concern only third-party cookies.

By default, **[browsers will enforce](https://www.chromestatus.com/feature/5088147346030592)** `SameSite=Lax` **on all cookies, both first-party and third-party, if the attribute is missing**. Here's Firefox Nightly on a first-party cookie:

> Cookie "get\_frog\_simplecookiename" has "sameSite" policy set to "lax" because it is missing a "sameSite" attribute, and "sameSite=lax" is the default value for this attribute.

A `SameSite=Lax` cookie is sent back with **safe HTTP methods**, namely GET, HEAD, OPTIONS, and TRACE. **POST requests instead won't carry the cookie.**

Third-party cookies with `SameSite=Strict` instead will be rejected altogether by the browser.

To recap, **here's the browser's behaviour for the different values of** `SameSite`:

| VALUE | INCOMING COOKIE | OUTGOING COOKIE |
|-------------|-------------|-----|
| Strict | Reject | - |
| Lax | Accept | Send with safe HTTP methods |
| None + Secure | Accept | Send |

To learn more about `SameSite` and to understand in detail all the use cases for this attribute, go read these fantastic resources:

- [Prepare for SameSite Cookie Updates](https://blog.heroku.com/chrome-changes-samesite-cookie)
- [SameSite cookies explained](https://web.dev/samesite-cookies-explained/)
- [SameSite cookie recipes](https://web.dev/samesite-cookie-recipes/)
- [Tough Cookies](https://scotthelme.co.uk/tough-cookies/)
- [Cross-Site Request Forgery is dead!](https://scotthelme.co.uk/csrf-is-dead/) 
- [CSRF is (really) dead](https://scotthelme.co.uk/csrf-is-really-dead/)

## Cookies and authentication

Authentication is one of the most challenging tasks in web development. There seems to be so much confusion around this topic, as token based authentication with JWT seems to supersede "old", solid patterns like **session based authentication**.

Let's see what role cookies play here.

### Session based authentication

**Authentication** is one of the most common use case for cookies.

When you visit a website that requests authentication, on credential submit (through a form for example) the backend sends under the hood a `Set-Cookie` header to the frontend.

A typical session cookie looks like the following:

```text
Set-Cookie: sessionid=sty1z3kz11mpqxjv648mqwlx4ginpt6c; expires=Tue, 09 Jun 2020 15:46:52 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
```

In this `Set-Cookie` header the server may include a cookie named **session, session id, or similar**.

This is the only identifier that the browser can see in the clear. Any time the **authenticated user requests a new page to the backend, the browser sends back the session cookie**.

At this point the backend pairs the session id with the session stored on a storage behind the scenes to properly identify the user.

Session based authentication is know as **stateful** because the backend has to keep track of sessions for each user. The storage for these sessions might be: 

- a database
- a key/value store like Redis
- the filesystem

Of these three session storages, Redis or the like should be preferred over database or filesystem.

Note that session based authentication has **nothing to do with the browser's Session Storage**. 

It's called **session based** only because the relevant data for user identification lives in the backend's session storage, which is not the same thing as a browser's Session Storage. 

### When to use session based authentication?

Use it **whenever you can**. **Session based authentication is one of the simplest, secure, and straightforward form of authentication for websites**. It's available by default on all the most popular web frameworks like Django.

But, its **stateful** nature is also its main drawback, especially when a website is served by a load balancer. In this case, techniques like **sticky sessions**, or **storing sessions on a centralized Redis storage** can help.

### A note on JWT

**JWT, short for JSON Web Tokens**, is an authentication mechanism, rising in popularity in recent years.

JWT is well suited for single page and mobile applications, but it presents a new set of challenges. The typical flow for a frontend application wanting to authenticate against an API is the following:

1. Frontend sends credentials to the backend
2. Backend checks credentials and sends back a token
3. Frontend sends the token on each subsequent request

The main question which comes up with this approach is: **where do I store this token in the frontend for keeping the user logged in?**

The most natural thing to do for someone who writes JavaScript is to save the token in `localStorage`. This **is bad for so many reasons**.

`localStorage` is easily accessible from JavaScript code, and it's an **easy target for XSS attacks**.

To overcome this issue, most developers resort to save the **JWT token in a cookie** thinking that `HttpOnly` and `Secure` can protect the cookie, at least from XSS attacks.

The new `SameSite` attribute, set to `SameSite=Strict`  would also protect your "cookified " JWT from CSRF attacks. But, is **also completely invalidates the use case for JWT in first instance** because `SameSite=Strict` does not sends cookies on cross-origin requests!

How about `SameSite=Lax` then? This mode allows sending cookies back with **safe HTTP methods**, namely GET, HEAD, OPTIONS, and TRACE. POST requests won't transmit the cookie either way.

Really, storing a JWT token in a cookie or in `localStorage` are both **bad ideas.** 

If you really want to use JWT instead of sticking with session based auth, and scaling your session storage, **you might want to use JWT with refresh tokens** to keep the user logged in.

Resources:

- [The Ultimate Guide to handling JWTs on frontend clients (GraphQL)](https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/)
- [Stop using JWT for sessions](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/)
- [Please, stop using localStorage](https://www.rdegges.com/2018/please-stop-using-local-storage/)

## Wrapping up

HTTP cookies have been there since 1994. They're everywhere.

Cookies are simple text strings, but they can be fine tuned for permissions, with `Domain` and `Path`, transmitted only over HTTPS with `Secure`, hide from JavaScript with `HttpOnly`.

A cookie might be used for personalization of the user's experience, user authentication, or shady purposes like tracking.

But, for all the intended uses, **cookies can expose users to attacks and vulnerabilities**. 

Browser's vendors and the Internet Engineering Task Force have worked year after year to improve cookie security, the last recent step being `SameSite`.

So what makes a secure cookie? There isn't such a thing. We could consider relatively secure a cookie that:

- travels only over HTTPS, that is, has `Secure`
- has `HttpOnly` whenever possible
- has the proper `SameSite` configuration
- does not carry sensitive data

Thanks for reading!

## Further resources

- [IETF on cookies](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-02)
- [Set-Cookie on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie)
- [HTTP Cookies on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)

Icon in the featured picture by [freepik](https://www.flaticon.com/authors/freepik).