Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine Best Auth Solution #828

Open
rmorshea opened this issue Nov 1, 2022 · 19 comments
Open

Determine Best Auth Solution #828

rmorshea opened this issue Nov 1, 2022 · 19 comments
Assignees
Labels
priority-2-moderate Should be resolved on a reasonable timeline. type-investigation About research and gathering information

Comments

@rmorshea
Copy link
Collaborator

rmorshea commented Nov 1, 2022

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 further HTTP requests and therefore no further headers will be sent. However, there are some ways you could try to work around this:

  1. You could push some Javascript via html.script 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 kinda ugly. 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.

@rmorshea rmorshea added the flag-triage Not prioritized. label Nov 1, 2022
@Archmonger
Copy link
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.

@rmorshea rmorshea added type-feature About new capabilities priority-2-moderate Should be resolved on a reasonable timeline. type-investigation About research and gathering information and removed flag-triage Not prioritized. labels Nov 1, 2022
@rmorshea rmorshea added this to the 2.0 milestone Nov 1, 2022
@Archmonger
Copy link
Contributor

Archmonger commented Nov 3, 2022

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()

@Archmonger
Copy link
Contributor

Archmonger commented Nov 4, 2022

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.

@rmorshea rmorshea modified the milestones: 2.0, 1.0 Nov 4, 2022
@rmorshea rmorshea changed the title Make Auth Easier Determine Best Auth Solution Nov 4, 2022
@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 4, 2022

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

@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 4, 2022

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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 4, 2022

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

@rmorshea rmorshea removed the type-feature About new capabilities label Nov 4, 2022
@Archmonger
Copy link
Contributor

However, that doesn't really provide components the user's authentication information, nor does it give a way to login/logout/validate a user session within IDOM components.

Feels a bit disjointed to me.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 5, 2022

Ok, so I did a bit more reading and I found that option (4) is an established pattern. There are examples of server-side session managers for frameworks like Flask via databases like Redis. This makes me more confident that we could go with that approach without completely reinventing things.

@Archmonger
Copy link
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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 5, 2022

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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Nov 5, 2022

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.

@Archmonger Archmonger modified the milestones: 1.0, 2.0 Dec 30, 2022
@rmorshea rmorshea removed this from the Luxury milestone Feb 21, 2023
@ajvogel
Copy link

ajvogel commented Jun 7, 2023

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

@Archmonger
Copy link
Contributor

Archmonger commented Jun 7, 2023

@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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Jun 7, 2023

@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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Jun 7, 2023

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

@rmorshea rmorshea self-assigned this Jul 16, 2023
@Archmonger
Copy link
Contributor

Archmonger commented Jul 28, 2023

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.

@rmorshea
Copy link
Collaborator Author

rmorshea commented Jul 28, 2023

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.

@ipcloudlive
Copy link

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

@Archmonger
Copy link
Contributor

Archmonger commented Feb 23, 2024

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 so 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
Labels
priority-2-moderate Should be resolved on a reasonable timeline. type-investigation About research and gathering information
Projects
None yet
Development

No branches or pull requests

4 participants