Search code examples
pythonsessionherokuflaskpeewee

Strange session behaviour with a Flask app on Heroku


I have a web application that uses GitHub's OAuth API in order to allow the app's users to interact with GitHub. However, I'm seeing some very odd behaviour with regards to the session cookie.

As a bit of background, I am using peewee to interface with Heroku's Postgres server, and have a User model like so:

class User(peewee.Model):
    login = peewee.TextField(unique=False)
    token = peewee.TextField()

I am using the web application flow described in the GitHub OAuth documentation, and am successfully getting called back with an access token, which I store in the database, and also in the session [1]:

@app.route('/callback')
def finishlogin():
    # I've verified that `token` and `login` are both valid at this point
    user = User.create(login=login, token=token)

    session['token'] = token

    return redirect(url_for('home'))

My route for home is as follows:

@app.route('/')
def home():
    if 'token' in session:
        user = User.get(token=session.get('token'))

        return 'Your login is {}'.format(user.login)
    else:
        # ...

So far, so good, and this works correctly. However, I am experiencing instances of users logging in, refreshing the page and finding that they are suddenly logged in as someone else. Logging the requests to the app shows that on the second request the session cookie itself has sent the wrong value (i.e. session.get('token') in home() returns a valid, but incorrect value. Clearly the user's browser can't know any other session value, so it seems that there is some "leakage" in setting the session between different clients and requests.

I'm not sure what the problem might be. My database is stored on the Flask g object as described in the peewee docs and has before_request and teardown_request hooks set up to open and close the database connection, and from all the documentation and example code I have read (and I've read a lot!), I seem to be using the session object correctly. I have set up a working secret_key for the session store.

I'm wondering if this could be something going on with Heroku and their routing mesh? But then, how would one user suddenly send another user's session?

Any hints or advice would be appreciated—I've been staring at this for a long time and am at my wits' end.

[1] I'm aware that storing the token directly is a bad design choice. The application is non-public and this will be fixed, but for now I want to describe the problem as it exists, even though it's not ideal.


Solution

  • Answering my own question for future reference.

    It seems that this was being caused by Flask's default session cookie behaviour, which is to send a Set-Cookie header with every single request, even for static assets. Our local Squid proxy was therefore gladly caching those requests and re-issuing Set-Cookie headers for every user.

    Setting Cache-Control headers for the whole app seems to fix the issue.