Repository: encode/starlette

Issue:
request.delete_cookie fails to delete samesite='none' cookies
### Checklist

- [Y] The bug is reproducible against the latest release and/or `master`.
- [Y] There are no similar issues or pull requests to fix it yet.

### Describe the bug

Calling `response.delete_cookie` in a route handler results in a response with a set-cookie header that has `sameSite='lax'`.

This set-cookie header is ignored by browsers if the cookie was being used in a cross-site context (i.e., the cookie was originally set via an xhr response with samesite='none'). The end result is that the cookie is not deleted.


### To reproduce

```python
# api.py

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import PlainTextResponse
from starlette.routing import Route


def home(request):
    return PlainTextResponse('Hello world!')

def login(request):
    response = PlainTextResponse('Logged in')
    response.set_cookie('token', 'abc', max_age=18000, httponly=True, secure=True, samesite='none')
    return response

def logout(request):
    response = PlainTextResponse('Logged out')
    response.delete_cookie('token')
    return response

routes = [
    Route('/', home),
    Route('/login', login, methods=['GET', 'POST']),
    Route('/logout', logout, methods=['GET', 'POST']),
]

middleware = [
    Middleware(
        CORSMiddleware,
        allow_origins=['http://localhost:9001'],
        allow_credentials=True,
        allow_methods=['*'],
        allow_headers=['*']
    )
]

app = Starlette(debug=True, routes=routes, middleware=middleware)
```

```html
<!-- index.html -->

<!DOCTYPE html>
<html>
  <head>
    <title>Starlette CORS cookie test</title>
  </head>
  <body>
    <div id="content"><button id="loginBtn">Login</button></div>

    <script>
      // CHANGE AS NECESSARY
      const API_URL = "https://dbdcec9f8fd9.ngrok.io";

      document.querySelector("#loginBtn").addEventListener("click", login);

      async function login() {
        await fetch(`${API_URL}/login`, {
          method: "POST",
          credentials: "include",
        });
        document.querySelector(
          "#content"
        ).innerHTML = `<button id="logoutBtn">Logout</button>`;
        document.querySelector("#logoutBtn").addEventListener("click", logout);
      }

      async function logout() {
        await fetch(`${API_URL}/logout`, {
          method: "POST",
          credentials: "include",
        });
        document.querySelector(
          "#content"
        ).innerHTML = `<button id="loginBtn">Login</button>`;
        document.querySelector("#loginBtn").addEventListener("click", login);
      }
    </script>
  </body>
</html>
```

1. Start the API server locally with `uvicorn api:app --port 9000`
2. Make the API available at a different site. An easy way is to use ngrok with `ngrok http 9000`. Let's say the API is now at https://dbdcec9f8fd9.ngrok.io
3. Change `API_URL` in index.html to point to the API
4. Serve index.html with `python -m http.server 9001`
5. Navigate to http://localhost:9001 and click the login button
6. Open devtools->Application and notice that the cookie is set
7. Click the logout button
8. Open devtools->Application and notice that the cookie is still set
9. (chrome-only) In devtools->Network, the response headers section for the /logout request has a warning indicator whose tooltip says the set-cookie header has been blocked. See screenshot below

![Screenshot_20210622_133455](https://user-images.githubusercontent.com/4007598/122887642-bad77100-d35e-11eb-9cb4-2df0290e1935.png)

### Expected behavior

Cookie is deleted

### Actual behavior

Cookie is not deleted

### Environment

- OS: Linux
- Python version: 3.8.5
- Starlette version: 0.14.2


Discussion:
First of all, deleting a cookie is the same as setting a cookie expired immediately, so the issue can be simplified to whether a cross-site `Set-Cookie` header is blocked under different `SameSite` values.

- Strict: Blocked.  Cross-site is not allowed.
- Lax: Blocked. Cross-site is not allowed.
- Not specifying `SameSite` attributes: Blocked. It's set to [default value `Lax`](https://www.chromestatus.com/feature/5088147346030592) (with some exception on POST forms, but unrelated to this issue).
- None: `Secure=true` must be present in Chrome; otherwise [it's blocked](https://www.chromium.org/updates/same-site).

Therefore, the following lines will not work because it's blocked by Chrome.

https://github.com/encode/starlette/blob/42592d68e5d7fd044b79d7846eabe7f33961527c/starlette/responses.py#L128-L129

To delete a cookie from a cross-site frame, `Secure=true` and `SameSite=none` are required.

```python
response.set_cookie('token', expires=0, max_age=0, secure=True, samesite='none')
```

However, the behavior is Chrome-specific. Firefox and Edge have different implementation on this.

I suggest we can either do
1. Deprecate `delete_cookie`: just use `set_cookie` to delete cookies
2. Allow cookie options in `delete_cookie` like [ExpressJs](https://github.com/expressjs/express/blob/508936853a6e311099c9985d4c11a4b1b8f6af07/lib/response.js#L801-L805) and [Werkzrug](https://github.com/pallets/werkzeug/blob/af160e0b6b7ddd81c22f1652c728ff5ac72d5c74/src/werkzeug/sansio/response.py#L250-L281) did.

2 is preferred for me.





That makes sense, and I prefer #2 too.

Does changing `delete_cookie` to the following make sense?

```
def delete_cookie(self, key: str, **kwargs) -> None:
        self.set_cookie(key, expires=0, max_age=0, **kwargs)
```

Resolving patch:
diff --git a/starlette/responses.py b/starlette/responses.py
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -125,8 +125,25 @@ def set_cookie(
         cookie_val = cookie.output(header="").strip()
         self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
 
-    def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
-        self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)
+    def delete_cookie(
+        self,
+        key: str,
+        path: str = "/",
+        domain: str = None,
+        secure: bool = False,
+        httponly: bool = False,
+        samesite: str = "lax",
+    ) -> None:
+        self.set_cookie(
+            key,
+            max_age=0,
+            expires=0,
+            path=path,
+            domain=domain,
+            secure=secure,
+            httponly=httponly,
+            samesite=samesite,
+        )
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         await send(


Test patch: