Repository: gratipay/gratipay.com

Issue:
can't stay logged in
from @amaltson via [Twitter](https://twitter.com/amaltson/status/332678066985893889):

> trying to log in with GitHub, each time I go to my account page, I get logged out.. 



Discussion:
Thanks for opening the issue. I signed in with my GitHub and that linked the account OK. However, I'm getting the following behaviour:
1. Go to Gittip.com
2. Login with GitHub
3. Click on my account page
4. Navigate to my account and now I'm logged out.
5. Log in again with GitHub, but now I'm in my profile page.
6. Click credit card to add a credit card.
7. Get the following: _Your credit card is . . . When you don't have enough money in your Gittip account to cover your gifts, we can attempt to pull money in using a credit card. If your credit card is missing or fails then your gifts won't go through for that week._ and now I'm again logged out and get asked to log in.

When I try to link my Twitter account (which I thought might work better, I get _Sorry, we can't find the page that you are searching for._. Furthermore, it created another, separate account, from my twitter login.

The separate Twitter account we can fix once we fix the underlying issue. My initial hypothesis is some cookie timing issue? Does it always log you out on the first page you try to hit immediately after you log in? I think that's what I'm hearing you say.

Yes, on the first page I hit after logging in. I don't think it's a cookie issue because I've had the same behaviour on two separate machines.

@whit537 still not able log in :(

Possibly related to #865.

I'm sorry for not making progress on this. I'm not sure where to begin debugging ...

@whit537 i have a vague memory that you said something about carrying the user token to third party systems in memory. (You and I were talking about limitations preventing running multiple Gittip Aspen servers.) That third party token may only be valid for a specific browser session, not globally for a single user. (I'm making a bunch of guesses in all of this. I haven't even managed to enumerate my assumptions.)

Store sessions in the database?  A key store would probably do it better, but it's a common solution to this

Edit: Misunderstood the issue.  Thought it was a multiple servers issue

Huh, it does actually work in-cognito in Chrome and works in Firefox. I've tried removing all the Local Storage, Session Storage and Cookies in non-in-cognito Chrome and still doesn't keep me logged in. I guess it must be a Chrome profile thing because it's broken in Chrome across three separate computers.

In-cognito is a fine workaround for now. Thanks!

+1 from @ceboudreaux in [IRC](https://botbot.me/freenode/gittip/msg/4259604/).

In-cognito windows allow cookies to be set. 

@amaltson Did you forbid accepting cookies in you profile?

+1 from @meatballhat on #865.

+1 from @eric-s-raymond on #1481.

I believe the email at https://github.com/gittip/www.gittip.com/issues/54#issuecomment-24810872 also includes a +1 here.

@bruceadams The third-party token I was referencing there was a one-time token used for oauth w/ Twitter. The actual session token is stored in the database. My understanding here is that people are getting a session token but it's getting nulled out too soon.

@eric-s-raymond at least has a null session token in the database.

@eric-s-raymond Do you see this same behavior if you use Firefox's private browsing feature? @meatballhat and @amaltson have reported Chrome's incognito mode as a workaround. Obviously we need to get to the bottom of this but if a similar workaround works for you in Firefox then that's an interesting clue.

[IRC brainstorming](https://botbot.me/freenode/gittip/msg/6222720/)

Here's the test case we've boiled down [IRC](https://botbot.me/freenode/gittip/msg/6224211/):

**Steps**
1. Visit https://www.gittip.com/ with a blank profile.
2. Click "Sign in > Twitter"
3. Authorize at Twitter and return to Gittip. Should now be back on Gittip.com with your username at upper right corner -- signed in.
4. Click "refresh."

**Expected:** Still have username at upper right corner -- signed in.
**Actual:** No username at upper right corner -- signed out.

@eric-s-raymond is getting the "Actual" result when he uses his default Firefox profile and a private window. He's getting the "Expected" result in a blank profile (`firefox -p`).

I do not see this behavior with my default profile running Firefox Aurora 26.0a2 (2013-09-21) on 64bit Linux. This probably aligns with what @eric-s-raymond sees in a blank profile. I'll try fiddling with my settings. I'll appreciate any guesses of what settings might be relevant.

@bruceadams We tried finding a difference in cookie settings that was relevant but he's got 59MB of diff for that ([IRC](https://botbot.me/freenode/gittip/msg/6225036/)). O.o

Our next step is to look at the behavior of the session cookie between the default and blank profiles. We want to look at both request and response cookies after steps 3 and 4.

[IRC](https://botbot.me/freenode/gittip/msg/6225274/)

I highly recommend the Firefox plugin _Live HTTP Headers_ https://addons.mozilla.org/en-US/firefox/addon/live-http-headers/

It can record request and response headers and is happy to write that sequence out to a simple text file after recording.

Records of the headers for the working and failing case would be tremendously helpful.

@meatballhat @amaltson @ceboudreaux @eric-s-raymond Would you be willing and able to provide a log of the header changes for the test case above?

For those of you working with chrome/chromium, its stock dev tools can save the log. Just right click in the netpanel and do "Copy all as HAR" to get great log viewable for example on http://www.softwareishard.com/har/viewer/.

To get HAR from Firefox you need to install Firebug extension and then netexport extension of the Firebug extension (http://www.softwareishard.com/blog/netexport/). After that few simple steps get us what we need:
1. load https://www.gittip.com/
2. open firebug (F12), select netpanel, click Persist button
3. sign in to gittip
4. press Ctrl+F5 to do a hard refresh of gittip
5. on the netpanel, select Export->Save as, as put that file somewhere for us to see

Best would be to do it for both, working and failing cases.

@eric-s-raymond has provided us with the logs from LiveHTTPHeaders for the [working](http://paste.ubuntu.com/6138138/) and [failing](http://paste.ubuntu.com/6138134/) cases. Analysis shows that the problem is manifested by extension DoNotTrackMe that tries to tell the gittip server, that the user does not want to be tracked. This request can be seen on line 355 of the failing case. It is handled by https://github.com/gittip/www.gittip.com/blob/master/gittip/__init__.py#L75 which redirects to https. Somehow we reset the session cookie in this case (we should not).

I am not sure how exactly the aspen hooks work, but if raising redirect causes the rest of inbound hooks to be skipped **but** the outbound hooks are still called - that'd be the cause of the problem. `gittip.canonize` is first in line, even before `gittip.security.authentication.inbound` which takes the session cookie and sets up user instance. After that the outbound hook `gittip.security.authentication.outbound` looks for this user and if it does not find it, it resets the session cookie.

@zwn You're reading it right. I'll take a look ...

My hunch is that the way to fix this is to modify the authentication hooks such that outbound is only run if inbound was run. That way we can have it as a[n eventually-documented] policy that any inbound hooks that preceed auth may raise without touching auth on the way out. This could actually be a good generalization for aspen hook pairs, either as a convention, or (less likely) an enforcement.

First I'm going to write a test that triggers canonization and then another one that reproduces the problem here. I'd also like to repro it TTW for myself.


Resolving patch:
diff --git a/gittip/security/authentication.py b/gittip/security/authentication.py
--- a/gittip/security/authentication.py
+++ b/gittip/security/authentication.py
@@ -3,6 +3,7 @@
 import rfc822
 import time
 
+import gittip
 from aspen import Response
 from gittip.security import csrf
 from gittip.security.user import User
@@ -105,3 +106,5 @@ def outbound(response):
     cookie['path'] = '/'
     cookie['expires'] = rfc822.formatdate(expires)
     cookie['httponly'] = "Yes, please."
+    if gittip.canonical_scheme == 'https':
+        cookie['secure'] = "Yes, please."


Test patch:
diff --git a/gittip/testing/client.py b/gittip/testing/client.py
--- a/gittip/testing/client.py
+++ b/gittip/testing/client.py
@@ -47,7 +47,7 @@ class TestClient(object):
     def __init__(self):
         self.cookies = SimpleCookie()
 
-    def get_request(self, path, method="GET", body=None,
+    def get_request(self, path, method="GET", body=b'',
                     **extra):
         env = StubWSGIRequest(path.encode('utf8'))
         env[b'REQUEST_METHOD'] = method.encode('utf8')
@@ -102,5 +102,5 @@ def post(self, path, data, user=None, content_type=MULTIPART_CONTENT,
         return self.perform_request(request, user)
 
     def get(self, path, user=None, **extra):
-        request = self.get_request(path, "GET")
+        request = self.get_request(path, "GET", **extra)
         return self.perform_request(request, user)
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
new file mode 100644
--- /dev/null
+++ b/tests/test_hooks.py
@@ -0,0 +1,82 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import os
+from gittip import wireup
+from gittip.testing import Harness
+from gittip.testing.client import TestClient
+from gittip.models.participant import Participant
+
+
+class Tests(Harness):
+
+    def setUp(self):
+        self.client = TestClient()
+        Harness.setUp(self)
+
+        self._blech = ( os.environ['CANONICAL_SCHEME']
+                      , os.environ['CANONICAL_HOST']
+                       )
+        os.environ['CANONICAL_SCHEME'] = 'https'
+        os.environ['CANONICAL_HOST'] = 'www.gittip.com'
+        wireup.canonical()
+
+    def tearDown(self):
+        Harness.tearDown(self)
+        os.environ['CANONICAL_SCHEME'] = self._blech[0]
+        os.environ['CANONICAL_HOST'] = self._blech[1]
+        wireup.canonical()
+
+
+    def test_canonize_canonizes(self):
+        response = self.client.get("/", HTTP_HOST='www.gittip.com', HTTP_X_FORWARDED_PROTO='http')
+        assert response.code == 302
+        assert response.headers['Location'] == 'https://www.gittip.com/'
+
+
+    def test_session_cookie_set_in_auth_response(self):
+        self.make_participant('alice')
+
+        # Make a normal authenticated request.
+        normal = self.client.get( "/"
+                                , user='alice'
+                                , HTTP_X_FORWARDED_PROTO='https'
+                                , HTTP_HOST='www.gittip.com'
+                                 )
+        alice = Participant.from_username('alice')
+        assert normal.headers.cookie['session'].value == alice.session_token
+
+
+    def test_session_cookie_is_sent_for_http_as_well(self):
+        # https://github.com/gittip/www.gittip.com/issues/940
+
+        self.make_participant('alice')
+
+        # Now make a request that canonizer will redirect.
+        redirect = self.client.get( "/"
+                                  , user='alice'
+                                  , HTTP_X_FORWARDED_PROTO='http'
+                                  , HTTP_HOST='www.gittip.com'
+                                   )
+        assert redirect.code == 302
+        assert redirect.headers.cookie['session'].value == ""
+
+        # This is bad, because it means that the user will be signed out of
+        # https://www.gittip.com/ if they make a request for
+        # http://www.gittip.com/. They might do this themselves accidentally,
+        # but more likely a browser plugin (such as DoNotTrack) will do it for
+        # them. The way we fix this is to set "secure" on the session cookie,
+        # so that the browser won't send the session cookie to the server in
+        # the case of http://www.gittipcom/. Without a session cookie in the
+        # request, gittip.security.authentication.outbound won't set one on the
+        # way out.
+
+
+    def test_session_cookie_is_secure_if_it_should_be(self):
+        # https://github.com/gittip/www.gittip.com/issues/940
+        response = self.client.get( "/"
+                                  , user=self.make_participant('alice').username
+                                  , HTTP_X_FORWARDED_PROTO='https'
+                                  , HTTP_HOST='www.gittip.com'
+                                   )
+        assert response.code == 200
+        assert '; secure' in response.headers.cookie['session'].output()