Skip to content

Determine Best Auth Solution #828

Open

Description

@rmorshea

Current Situation

This stems from discussion in #768

As explained in https://github.com/phihos/idom-auth-example-sanic, the problem we need to solve is how to securely authenticate users inside an ReactPy single page application. Usually authentication is done via Cookie or Authorization header on each HTTP request. But after the websocket connection has been established, no future headers will be sent. However, there are some ways you could try to work around this:

  1. The user could push some Javascript that sends a separate auth request to your auth API and then reloads the page to reestablish the websocket connection with new auth headers, but this is not an elegant solution. It defeats the purpose of ReactPy not having to write any Javascript and having a visible reload has a negative impact on the user-experience.

  2. You could also render the login page traditionally and then redirect to a new page with embedded ReactPy . But then you already split your application into two parts: "pre-auth" with traditional server-side template rendering and "post-auth" with ReactPy. Keeping both parts consistent is probably not fun.

  3. You can also do authentication inside the single page app and save the auth state via use_state. But it will be gone as soon as a websocket disconnect happens. You can mitigate this by pushing some Javascript that sets a session cookie. But now there is a new problem: Session cookies should be set with the HttpOnly flag to prevent XSS attacks from recovering the session cookie. This can not be done (or at least is difficult to do) with Javascript. So you might end up with a security flaw in your app.

  4. Since you have at least one full HTTP request-response cycle you can set a session cookie with a session ID on that response if the request does not already contain a cookie with a valid session ID. That ensures that the following request for the websocket connection always contains a session ID cookie. With use_request we can extract the session ID and then the server can retrieve the session data. In that data we can look up the authentication state and let ReactPy display a login form or the actual content. We can later manipulate the session data to perform a login or logout. All without the need to set a further cookie or push Javascript - provided we implement a server-side session. A rough prototype for this has been implemented here based on work done in https://github.com/phihos/idom-auth-example-sanic.

Proposed Actions

Explore the viability of each option.

Activity

  1. Archmonger commented on Nov 1, 2022

    @Archmonger
    Contributor

    As a note, Conreq currently uses approach 2.

    Handling this will likely need us to dive deep into the authentication systems of each framework we support.

  2. added
    priority-2-moderateShould be resolved on a reasonable timeline.
    type-investigationAbout research and gathering information
    and removed on Nov 1, 2022
  3. added this to the milestone on Nov 1, 2022
  4. Archmonger commented on Nov 3, 2022

    @Archmonger
    Contributor

    Maybe the API will look something like this:

    @component
    def my_component():
        auth_manager = hooks.use_auth()
    
        # Returns a IDOM session dataclass that contains any auth data, such the active user
        print(auth_manager.session)
    
        # Direct pass-through to the backend framework's authentication system
        # but also sets cookies within the client. This will likely require us to allow kwargs.
        auth_manager.login(username="username", password="password")
    
        # Direct pass-through to the backend framework's authentication system.
        # Will also need to invalidate cookies within the client
        auth_manager.logout()
    
        # Attempts to re-validate whether the user is currently logged in (cookies still valid)
        # and returns a boolean. This is needed due to the difference between HTTP response cycles
        # versus websocket persistent connections
        auth_manager.validate_session()
  5. Archmonger commented on Nov 4, 2022

    @Archmonger
    Contributor

    Should we should beta test this as a standalone hook within django-idom, and migrate it to core after IDOM hits v1? Similar to how we are currently moving use_location/use_websocket/etc hooks to core after first being developed in django-idom.

    If so then I can draft a version of this fairly quickly.

  6. modified the milestones: , 1.0 on Nov 4, 2022
  7. changed the title [-]Make Auth Easier[/-] [+]Determine Best Auth Solution[/+] on Nov 4, 2022
  8. rmorshea commented on Nov 4, 2022

    @rmorshea
    CollaboratorAuthor

    I updated the issue description here. I think we need to explore these options more deeply.

  9. rmorshea commented on Nov 4, 2022

    @rmorshea
    CollaboratorAuthor

    I'm not really satisfied that option (4), the one @phihos implemented, is ideal. I'm worried that, by going with that approach, we'll basically have to reinvent the wheel for every auth mechanism under the sun( Oauth, JTW, etc). I'm also concerned that, not being a security expert myself, that we'll get something wrong.

    On thinking about this further I actually think that a solution like option (1) is more viable than it might have seemed at the outset. The only real downside is that, if you try to do it with html.script, you need to refresh the page. However, with a custom JS component, you wouldn't need a page refresh.

    I mocked up a very simple <Request/> component that's compatible with IDOM's custom JS interface:

    // request.js
    export function bind(node, config) {
      return {
        create: (component, props) => () => component(props),
        render: () => undefined,
        unmount: () => undefined,
      };
    }
    
    export function Request(props) {
      fetch(props.request.url, props.request.info)
        .then((response) => response.text())
        .then(props.onResponse)
        .catch(props.onError);
    }
    # request.py
    from idom import component
    from idom.web.module import module_from_file, export
    
    @component
    def request(url, body, method, on_response):
        return _request_component(
            {
                "onResponse": on_response,
                "request": {
                    "url": url,
                    "info": {"body": body, "method": method},
                },
            }
        )
    
    _request_module = module_from_file(name="request", file="request.js")
    _request_component = export(_request_module, "Request")

    You could then embed this invisible component into your view to tell the client to hit some URL and trigger the onResponse callback with the data.

    from idom import component
    from idom.backend.fastapi import configure
    from fastapi import FastAPI
    from fastapi.responses import JSONResponse
    
    from hacky_request_component import request
    
    app = FastAPI()
    
    @app.post("/example")
    def example():
        return JSONResponse("hello", headers={"AuthToken": "fake"})
    
    @component
    def temp():
        return request(url="/example", body=None, method="POST", on_response=print)
    
    configure(app, temp)

    This would effectively allow you to use a standard auth flow.

  10. rmorshea commented on Nov 4, 2022

    @rmorshea
    CollaboratorAuthor

    I've tried the above out and it seems to work.

  11. 3 remaining items

  12. Archmonger commented on Nov 5, 2022

    @Archmonger
    Contributor

    I'm still not convinced this is a 1.0 milestone though, we're getting pretty deep into scope creep for the 1.0 release.

  13. rmorshea commented on Nov 5, 2022

    @rmorshea
    CollaboratorAuthor

    The scope of this issue is mostly focused on finding the best option. Not necessarily on implementing it. I'd at least like to have a prepared answer if someone asks and ideally have something documented.

  14. rmorshea commented on Nov 5, 2022

    @rmorshea
    CollaboratorAuthor

    I think you're right though. Most people who initially adopt IDOM are probably going to be making internal facing services that don't need app level auth.

  15. modified the milestones: 1.0, on Dec 30, 2022
  16. removed this from the milestone on Feb 21, 2023
  17. ajvogel commented on Jun 7, 2023

    @ajvogel

    Would using one of the backend auth libraries (such as flask-login) not work?

  18. Archmonger commented on Jun 7, 2023

    @Archmonger
    Contributor

    @ajvogel I'm not familiar with that specific library, but it depends on

    • How well the library interacts with running on an async websocket.
    • Whether the current user session gets prepopulated into the ASGI/WSGI scope (use_scope hook)

    Feel free to test it out and let us know.

  19. rmorshea commented on Jun 7, 2023

    @rmorshea
    CollaboratorAuthor

    @ajvogel, I suspect that flask-login might not help here unfortunately. flask-login would basically require solution number 2. In the long-run I think we're probably going to run with solution number 4.

  20. rmorshea commented on Jun 7, 2023

    @rmorshea
    CollaboratorAuthor

    @ajvogel, if this is blocking for you, can you upvote this issue? It will help us prioritize what we should work on.

  21. self-assigned this
    on Jul 16, 2023
  22. Archmonger commented on Jul 28, 2023

    @Archmonger
    Contributor

    I'm realizing we now have a new solution open to us given our "ReactPy as middleware" concept.

    Our auth solution can be implemented similar to the Django Channels AuthMiddleware, which effectively just adds a scope['user'] database model to the scope.

    That user object should have log out and log in methods attached to it.

  23. rmorshea commented on Jul 28, 2023

    @rmorshea
    CollaboratorAuthor

    That seems like more of a new interface than an implementation. Determining the identity of the user still requires one of the options list above.

  24. ipcloudlive commented on Dec 30, 2023

    @ipcloudlive

    We Need some thing like Cookie or LocalStorage or SessionStorage to solve auth problem
    Expecting the solution before New Year

  25. Archmonger commented on Feb 23, 2024

    @Archmonger
    Contributor

    I will be exploring authentication within ReactPy-Django, using that as our ground-zero for testing out authentication methods. This will require the use_messenger hook to get merged in.

    My cursory exploration tells me this will get really messy for adding support to every possible backend. It's only barely possible due to how Django is extremely batteries-included.

    The future of authentication for ReactPy Core likely involves giving some barebones instructions on how to use JavaScript (#894 and #1001) to accomplish auth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

priority-2-moderateShould be resolved on a reasonable timeline.type-investigationAbout research and gathering information

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions