MasoniteFramework / masonite

The Modern And Developer Centric Python Web Framework. Be sure to read the documentation and join the Discord channel for questions: https://discord.gg/TwKeFahmPZ
http://docs.masoniteproject.com
MIT License
2.22k stars 126 forks source link

Lost session after callback #788

Closed nok closed 9 months ago

nok commented 10 months ago

Describe the bug

I want to access some data of Twitch users. For that a user has to connect over a Twitch application. To get an authorization code (for further steps) I have to pass a callback which will return the code and a passed state (for security reasons). The action from the controller which accepts the callback doesn't return any previous saved data from the session. It's empty. Because of that it's impossible to compare the saved and passed states.

The following example shows the problem:

The (web) routes:

ROUTES = [
    Route.get("/", "WelcomeController@show").name("welcome"),
]

The WelcomeController:

import secrets

from masonite.views import View
from masonite.helpers import compact
from masonite.request import Request
from masonite.controllers import Controller
from masonite.sessions import Session

from app.components.twitch.social import create_connect_url

class WelcomeController(Controller):
    def show(
        self,
        session: Session,
        request: Request,
        view: View,
    ):
        print(f"Session data at the start: {session.get_data()}")

        # Check if passed and stored state (from the session) are equal:
        params = request.all()
        has_passed_state = "state" in params.keys()
        has_stored_state = session.has("state")  # False after redirect
        if has_passed_state and has_stored_state:
            state_is_equal = params["state"] == session.get("state")
            if state_is_equal:
                message = "got equal states"
                return view.render("welcome", compact(message))

        # Generate Twitch connect URL:
        state = secrets.token_urlsafe(16)
        session.set("state", state)
        connect_url = create_connect_url(state=state)

        message = "click to connect via Twitch"
        print(f"Session data at the end: {session.get_data()}")
        return view.render("welcome", compact(message, connect_url))

And the welcome view:

{% extends "base.html" %}

{% block content %}

<p>{{ message }}</p>

{% if connect_url %}
<a href="{{ connect_url }}">Connect</a>
{% endif %}

{% endblock %}

This is an example of a connect_url: https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=MY_CLIENT_ID&redirect_uri=https://my_app.local&scope=user:read:email&state=tyQsWmpJ2NTMYI37alequw

The prints of the workflow are here:

Session data at the start: {}
Session data at the end: {'state': 'tyQsWmpJ2NTMYI37alequw'}
# (click on `Connect` link)
Session data at the start: {}

Expected behaviour

The session should keep the previous passed state. The prints should look like this:

Session data at the start: {}
Session data at the end: {'state': 'tyQsWmpJ2NTMYI37alequw'}
# (click on `Connect` link)
Session data at the start: {'state': 'tyQsWmpJ2NTMYI37alequw'}

Steps to reproduce the bug

No response

Screenshots

No response

OS

macOS

OS version

Monterey 12.2

Browser

Chromium 120

Masonite Version

4.19.0

Anything else ?

No response

eaguad1337 commented 10 months ago

I tested it and I can't reproduce what you described. Please give a working example to understand if there's an error. It might be related to your workflow or a third party library.

cercos commented 10 months ago

I had a similar issue and it was only with brave and chromium but it worked in Edge. It did randomly start preserving session data but something was creating a new session ID every page refresh SESSID would change and it shouldn't. I did however create a workaround when I was having issues with the sessions. I modified the redirect() method and has_invalid_state() method in the BaseDriver as follows (original method commented out):

def redirect(self, state=None):
    if self.use_state():
        # let user provide their own generated state
        state = state or self.get_state()
        # Store the state in a secure, HttpOnly cookie
        self.application.make('response').cookie('oauth_state', state, http_only=True, secure=False, samesite='Lax')

    else:
        state = None
    authorization_url = self.build_auth_url(state)
    print(authorization_url)
    response = self.application.make('response')
    response.status(302)
    response.header('Location', authorization_url)
    return response

# def redirect(self, state=None):
#     if self.use_state():
#         # let user provide their own generated state
#         state = state or self.get_state()
#         # self.application.make("session").set("state", state)
#         self.application.make("request").session.set("state", state)
#
#     else:
#         state = None
#     authorization_url = self.build_auth_url(state)
#     print(authorization_url)
#     return self.application.make("response").redirect(location=authorization_url)

def has_invalid_state(self):
    if self._is_stateless:
        return False
    # Retrieve the state from the cookie
    state = self.application.make('request').cookie('oauth_state')
    return state != self.application.make("request").input("state")

# def has_invalid_state(self):
#     if self._is_stateless:
#         return False
#     state = self.application.make("session").get("state")
#     return state != self.application.make("request").input("state")
josephmancuso commented 9 months ago

@eaguad1337 whats the status of this? was it resolved? do you understand whats going on here?

eaguad1337 commented 9 months ago

@josephmancuso I couldn't reproduce the issue. The reporter didn't respond anymore.

josephmancuso commented 9 months ago

will reopen if more info we can work on and replicate