Repository: falconry/falcon

Issue:
Improve set_cookie() documentation (and validation?) wrt domain name
Hi! 

My code is like:
```
        resp.status = '301 Moved Permanently'
        resp.set_header('Set-Cookie', 'user=abc') # With set_cookie result is same
        resp.set_header('Location', '...')
        print(resp._headers)
```

```resp._headers``` shows cookie header, but when I make request, there is no cookie. Is it known problem?

I'm using Python 3.5 and last pip version of falcon


Discussion:
Such functionality was earlier implemented on Flask and I didn't had such problem.

Hi @sevazhidkov, This could be due to [the secure attribute](http://falcon.readthedocs.io/en/latest/api/cookies.html#the-secure-attribute). I know that setting this to `False` during development is tedious; one solution may be to introduce an application-wide option to the framework for setting this so it doesn't have to be done for each call to `set_cookie()`.

@kgriffs Even with secure=False it still don't work.
I want to note, that host domain and redirection domain is different (it's oAuth authorization)

@sevazhidkov Is this still an issue for you? I’m having difficulty replicating the issue you’ve described. For me, the `set-cookie` header is coming through on both the cli and in Chrome. It makes me wonder if this is no longer an issue or there is another service between Falcon and your client that is stripping out the header. If you're still experiencing this behavior, could you provide more information regarding the environment Falcon is running in? 

For clarity sake, here is what I used when I attempted to replicate the issue:
* Python 3.5.2
* Gunicorn 19.6.0
* Falcon 1.1.0
* Httpie 0.9.4 (To make the requests)

Sample App `app.py`
```
import falcon
import json

class QuoteResource:
    def on_get(self, req, resp):
        """Handles GET requests"""
        resp.body = "{}"
        resp.status = '301 Moved Permanently'
        resp.set_header('Set-Cookie', 'user=abc')
        resp.set_header('Location', '...')
        print(resp._headers)

api = falcon.API()
api.add_route('/quote', QuoteResource())
```
Console Return:
```
(falcon-sample) $ gunicorn app:api
[2017-01-26 21:01:28 -0600] [20239] [INFO] Starting gunicorn 19.6.0
[2017-01-26 21:01:28 -0600] [20239] [INFO] Listening at: http://127.0.0.1:8000 (20239)
[2017-01-26 21:01:28 -0600] [20239] [INFO] Using worker: sync
[2017-01-26 21:01:28 -0600] [20242] [INFO] Booting worker with pid: 20242
{'set-cookie': 'user=abc', 'location': '…'}
```

Request:
```
(falcon-sample) $ http -v localhost:8000/quote
GET /quote HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/0.9.4



HTTP/1.1 301 Moved Permanently
Connection: close
Date: Fri, 27 Jan 2017 03:01:29 GMT
Server: gunicorn/19.6.0
content-length: 2
content-type: application/json; charset=UTF-8
location: ...
set-cookie: user=abc

{}
```

I also tried setting it with
```
resp.set_cookie('user', 'abcde', secure=False) 
```
and that worked as well.
Closing this issue for now. Please re-open this issue if you run into any issues. Thanks!
I'm trying to develop a user management interface for my project using the following:

- Python 2.7
- falcon 1.4.1
- falcon-cors 1.1.7
- gunicorn 19.9.0

I'm testing the login page in **Google Chrome** initiated by ```run``` command in **Pycharm Professional**.

**Operating System:** Ubuntu 18.04 LTS

```
import falcon
from falcon_cors import CORS
cors = CORS(allow_all_origins=True,allow_all_headers=True,allow_all_methods=True)

class User:
    def __init__(self):
        self.name = 'Users API'

    def on_post(self, req, resp):
        try:
            data = urlparse.parse_qs(req.stream.read())
            if "loginEmail" in data.keys() and "loginPassword" in data.keys():
                email = data['loginEmail'][0]
                password = data['loginPassword'][0]
                result = self.authenticate(email, password)
                if result["status"]=="Success":
                    resp.set_cookie('session', result["session"],path="/",domain="192.168.32.17:5000",secure=False)
            else:
                raise Exception, "Email or Password not provided"

            if len(result) <= 0:
                raise Exception, 'Empty Response'
            resp.body = json.dumps(result)

        except Exception as e:
            resp.body = json.dumps({'status': 'Error', 'message': e.message, 'details': str(e)})

    def on_get(self, req, resp):
        try:
            data = req.params
            cookies = req.cookies
            if "session" in cookies.keys():
                session = cookies['session']
                result=self.get(session,data)
                if len(result) <= 0:
                    raise Exception, 'Empty Response'
            else:
                raise Exception, "Session Terminated"

            resp.body = json.dumps(result)
        except Exception as e:
            resp.body = json.dumps({'status': 'Error','message': e.message,'details': str(e)})

api = falcon.API(middleware=[cors.middleware])
api.add_route('/user', User())
```

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Project</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body onload="refresh()">
<form method="post" action="http://192.168.32.17:5000/user" accept-charset="UTF-8" id="login-nav">
    <input type="email" name="loginEmail" placeholder="Email address" required>
    <input type="password" name="loginPassword" placeholder="Password" required>
    <button type="submit" id="btnLogin">Sign in</button>
 </form>
<script>
    $(document).ready(function() {
        $('#login-nav').submit(function(){
            $.post($(this).attr('action'), $(this).serialize(), function(json) {
                refresh()
            }, 'json');
            return false;
       });
    });

   function refresh(){
            $.get("http://192.168.32.17:5000/user",  { 'fields': [ "name", "dp" ] }, function(json) {
                if (json["status"]=="Error"){
                    alert(json["message"])
                }
               else {
                $('#dd-username').text(json["data"]["name"])
                $("#dd-photo").attr("src",json["data"]["dp"])
                }
            }, 'json');
    }

    function getCookie(cname) {
        var name = cname + "=";
        var decodedCookie = decodeURIComponent(document.cookie);
        var ca = decodedCookie.split(';');
        for(var i = 0; i <ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1);
            }
            if (c.indexOf(name) == 0) {
                return c.substring(name.length, c.length);
            }
        }
        return "";
    }

    function checkCookie() {
        var session = getCookie("session");
        if (session ==null || session==""){
            return false
        }
        else{
            return true
        }
</script>
</body>
</html>
```
On login request I'm getting required response as given below:
```
{"status": "Success", "message": "Session has been created successfully", "session": "F26F75C27942DE27"}
```

But on ```refresh()```, I'm getting following response:
```
{"status": "Error", "message": "Session Terminated"}
```
I also tried checking for cookie using ```chechCookie()``` method above and it returns ```false``` as well.

I also tried to check for cookies using "Right Click> Inspect Element > Network" and "Right Click> Inspect Element > EditThisCookie" but found it nowhere.

Then I tried setting cookie in browser using java script by modifying the script above as given below:
```
     $('#login-nav').submit(function(){
        $.post($(this).attr('action'), $(this).serialize(), function(json) {
            setCookie("session",json["session"])
            refresh()
        }, 'json');
        return false;
      });

    function setCookie(cname, cvalue, exdays) {
        var d = new Date();
        d.setTime(d.getTime() + (exdays*24*60*60*1000));
        var expires = "expires="+ d.toUTCString();
        document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
    }
```
Using this method, I was able to set cookies in browser. ```checkCookie()``` returned ```True```. 
I was also able to locate the cookie in EditThisCookie plugin of Google Chrome

BUT

When refresh() was called, it returned the same response "Session Terminated".
This means ```session``` was not found in ```req.cookies``` on server end.
@shehzi-khan I can indeed reproduce your problem using the provided test case (I took the liberty to edit it a bit and fill in the missing methods -- **warning** -- my examples are solely for illustration purposes and do not represent sound web development practices).

However, the issue is caused by a bug in this test case, not by a bug in the framework itself: as stated in [RFC 6265](https://tools.ietf.org/html/rfc6265), cookies are set on domains only, not ports.

See also this Stack Overflow answer for a more detailed interpretation of the RFC: https://stackoverflow.com/questions/1612177/are-http-cookies-port-specific

Upon rectifying that, the test case starts functioning as expected:

```python
import hashlib
import os
import random
import urlparse

import falcon
from falcon_cors import CORS

cors = CORS(allow_all_origins=True,
            allow_all_headers=True,
            allow_all_methods=True)

HOST = '192.168.32.17'
PORT = 5000

USER_LANGUAGES = ('C', 'C++', 'Golang', 'PHP', 'Python', 'Ruby', 'Rust')


class User(object):
    def __init__(self):
        self.name = 'Users API'
        self.data = {}

    def get(self, session, data=None):
        return self.data[session]

    def authenticate(self, email, password):
        success = email.strip() and password.strip()
        result = {'status': 'Success'} if success else {'status': 'Failure'}
        if success:
            session = hashlib.md5(password).hexdigest()
            result.update(session=session)
            language = random.choice(USER_LANGUAGES)
            if language == 'PHP':
                # Just no, please
                raise falcon.HTTPForbidden
            self.data[session] = {'language': language}

        return result

    def on_post(self, req, resp):
        try:
            data = urlparse.parse_qs(req.stream.read())
            if "loginEmail" in data.keys() and "loginPassword" in data.keys():
                email = data['loginEmail'][0]
                password = data['loginPassword'][0]
                result = self.authenticate(email, password)
                if result["status"] == "Success":
                    resp.set_cookie('session', result["session"],
                                    path="/",
                                    # see RFC 6265, section-8.5:
                                    # Cookies do not provide isolation by port
                                    # domain="{}:{}".format(HOST, PORT),
                                    domain=HOST,
                                    secure=False)
            else:
                raise Exception('Email or Password not provided')

            if len(result) <= 0:
                raise Exception('Empty Response')
            resp.media = result

        except falcon.HTTPForbidden:
            raise

        except Exception as e:
            raise falcon.HTTPBadRequest(title='Error', description=str(e))

    def on_get(self, req, resp):
        try:
            data = req.params
            cookies = req.cookies
            if "session" in cookies.keys():
                session = cookies['session']
                result = self.get(session, data)
                if len(result) <= 0:
                    raise Exception('Empty Response')
            else:
                raise Exception("Session Terminated")

            resp.media = result

        except Exception as e:
            raise falcon.HTTPBadRequest(title='Error', description=str(e))


api = falcon.API(middleware=[cors.middleware])
api.add_route('/user', User())
api.add_static_route('/app', os.path.dirname(__file__))
```

However, I realize this could be either stressed clearer in the documentation, and/or better domain name validation added in ``set_cookie()`` method. Hence labeling the issue as such.
I too am having issues getting `set_cookies` to work.

Here is my very simple code:

```
import falcon

class HelloResource(object):
    def on_get(self, req, resp):
        resp.status = falcon.HTTP_200
        resp.body = 'Hello World!'

class UserAuthenticationResource(object):
    def on_post(self, request: falcon.Request, response: falcon.Response):
        response.set_cookie('user', 'aaaaaa', secure=False)

app = falcon.API()

app.add_route('/', HelloResource())
app.add_route('/login', UserAuthenticationResource())
```

It's run with gunicorn: `gunicorn --bind 0.0.0.0:8888 path.to.file:app`.

I've done a lot of experimentation with `set_cookie` parameters and binding to `localhost` etc. yet nothing has worked. Using falcon 2.0.0 and gunicorn 19.9.0
Well this is very derpy/embarrassing. Last few frameworks I used didn't set `http_only` to `True` by default, and this one does. I just misread the Chrome cookie inspector the first few times before I realized my mistake. Sorry for the trouble.  🤦‍♂ 
I was having the same issue @graemedouglas.  Thanks for writing this, as it helped me resolve it.

Resolving patch:
diff --git a/falcon/response.py b/falcon/response.py
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -342,6 +342,10 @@ def set_cookie(self, name, value, expires=None, max_age=None,
                 domain must include the origin server. Otherwise, the
                 user agent will reject the cookie.
 
+                Note:
+                    Cookies do not provide isolation by port, so the domain
+                    should not provide one. (See also: RFC 6265, Section 8.5)
+
                 (See also: RFC 6265, Section 4.1.2.3)
 
             path (str): Scopes the cookie to the given path plus any
@@ -374,10 +378,16 @@ def set_cookie(self, name, value, expires=None, max_age=None,
 
                 (See also: RFC 6265, Section 4.1.2.5)
 
-            http_only (bool): Direct the client to only transfer the
-                cookie with unscripted HTTP requests
-                (default: ``True``). This is intended to mitigate some
-                forms of cross-site scripting.
+            http_only (bool): The HttpOnly attribute limits the scope of the
+                cookie to HTTP requests.  In particular, the attribute
+                instructs the user agent to omit the cookie when providing
+                access to cookies via "non-HTTP" APIs. This is intended to
+                mitigate some forms of cross-site scripting. (default: ``True``)
+
+                Note:
+                    HttpOnly cookies are not visible to javascript scripts
+                    in the browser. They are automatically sent to the server
+                    on javascript ``XMLHttpRequest`` or ``Fetch`` requests.
 
                 (See also: RFC 6265, Section 4.1.2.6)
 
@@ -501,6 +511,10 @@ def unset_cookie(self, name, domain=None, path=None):
                     domain must include the origin server. Otherwise, the
                     user agent will reject the cookie.
 
+                    Note:
+                        Cookies do not provide isolation by port, so the domain
+                        should not provide one. (See also: RFC 6265, Section 8.5)
+
                     (See also: RFC 6265, Section 4.1.2.3)
 
             path (str): Scopes the cookie to the given path plus any


Test patch: