loco-rs / loco

🚂 🦀 The one-person framework for Rust for side-projects and startups
https://loco.rs
Apache License 2.0
5.02k stars 207 forks source link

Add RequestContext, and support request-scoped sessions #561

Open jondot opened 5 months ago

jondot commented 5 months ago

We currently have not custom requestcontext, on which we can push a concept of a session. We currently are doing this kind of context by:

We want to let the user configure a persisted session (based on cookies or otherwise), and be able to store request-specific data on a per-request basis which forms the concept of a request context (we currently only provide an app context).

Usage can be extractor based such as:

fn handler(ctx: AppContext, req: RequestContext) { 
//req.session
//req.id
}

this opens up some tricky ergonomic questions such as:

Moving forward:

yinho999 commented 5 months ago

@jondot Would you mind assigning this ticket to me, please? I would like to give it a try

jondot commented 5 months ago

@yinho999 I'll be happy to. let's start with some draft of how the API will work?

yinho999 commented 5 months ago

@jondot I will provide a draft example with tower sessions to see how loco and tower sessions work together before working on the second task.

yinho999 commented 4 months ago

Hi @jondot,

Sorry for the late response. I crafted a simple Request Context example that contains the request ID and session in Axum, which we can easily migrate to Loco.

I found that creating and persisting the same request ID through the request and tracing middleware is a bit of a hassle. Here is my approach:

I extracted the ID creation from the tracing middleware(similar to loco's) into a standalone middleware. This is because tracing middleware is self-encapsulated, and I don't think it is possible to extend the request with an ID within it.

What do you think about this approach? If there is a better way, please let me know. :+1:

Draft Code

jondot commented 4 months ago

I think it's fantastic! the ergonomics look great. we can proceed with this, definitely.

A few pointers to think about:

Name of the context

We need to think about something short, but that differentiate itself from ctx which is app context. maybe req ?

foo_handle(context: RequestContext) -> { .. } foo_handle(req: RequestContext) -> { .. }

Configuring a store

This may be a big deal. Because if I remember correctly, in Rails, cookies are default. This is a very brave default to force on users. However if Rails does it -- I'm OK with doing exactly the same. I'm also pretty sure that we need some kind of secret in order to sign sessions, so this needs to come from configuration. Also, in Rails, initially they set a default cookie secret -- but that presented itself as a security flaw, then they moved into giving NO secret at all (one of the first steps you need to do is generate your own unique secret).

I would summarize:

Flash, Session, etc.

Rails provides a "convenience" hashes in the session:

Store vs Cookies

Note that Rails provides:

These two are different things, and can be implemented separately.

If you configure sessions to be stored on cookies -- you will have a single cookie on the user containing the data in addition to any other "custom" cookies a user set with the separate non-session cookie store. For the unique session store single cookie we need to pick a unique app-specific cookie ID (e.g. __loco_myapp_session)


If in doubt, always good to read how Rails does it: https://guides.rubyonrails.org/action_controller_overview.html#session Fantastic work, this should be exciting to see in Loco!

yinho999 commented 4 months ago

I think it's fantastic! the ergonomics look great. we can proceed with this, definitely.

A few pointers to think about:

Name of the context

We need to think about something short, but that differentiate itself from ctx which is app context. maybe req ?

foo_handle(context: RequestContext) -> { .. } foo_handle(req: RequestContext) -> { .. }

Configuring a store

This may be a big deal. Because if I remember correctly, in Rails, cookies are default. This is a very brave default to force on users. However if Rails does it -- I'm OK with doing exactly the same. I'm also pretty sure that we need some kind of secret in order to sign sessions, so this needs to come from configuration. Also, in Rails, initially they set a default cookie secret -- but that presented itself as a security flaw, then they moved into giving NO secret at all (one of the first steps you need to do is generate your own unique secret).

I would summarize:

  • Use default of cookie store for sessions. As per Loco's and Rust high configurability -- offer a section to easily switch providers (e.g. in memory or others).
  • When using cookie store, we need to use an app-specific secret, which is generated per app, no default

Flash, Session, etc.

Rails provides a "convenience" hashes in the session:

  • Flash - I believe this builds on top of a session, but has some kind of middleware to clear out -- it is something that lives only between actions. this may be too complex to do in Rust and Axum, so if too complicated, we can skip it
  • A normal session store that looks like a hash and is permanent e.g. session["foobar"] = 3, which I believe you already demonstrated

Store vs Cookies

Note that Rails provides:

  • Session: which can be backed by almost anything: inmemory, database, including on a cookie
  • Cookie store: which stores information on user cookies

These two are different things, and can be implemented separately.

If you configure sessions to be stored on cookies -- you will have a single cookie on the user containing the data in addition to any other "custom" cookies a user set with the separate non-session cookie store. For the unique session store single cookie we need to pick a unique app-specific cookie ID (e.g. __loco_myapp_session)


If in doubt, always good to read how Rails does it: https://guides.rubyonrails.org/action_controller_overview.html#session Fantastic work, this should be exciting to see in Loco!

Thanks for providing such detailed guidance and feedback! I am going to dive deep and study Ruby sessions and cookies. Just to mention, PrivateCookieJar might be a good fit for the cookie store based on the description.

yinho999 commented 4 months ago

@jondot Sorry for the late reply, I was unavailable last week due to illness. I was exploring the cookie section of tower-sessions and discovered that it stores only the session identifier within the cookie, not the data, due to security concerns.

Currently, I am considering recreating a session using PrivateCookieJar as the default method. We can swap it out for tower-sessions when the user selects a different store/backend. How does that sound?

jondot commented 4 months ago

No worries. My take on it, is that tower-sessions are right. storing data in the cookie over insecure channel is a risk.

The way to mitigate it is:

  1. Run SSL in production
  2. Encrypt + sign cookies

And this is the default in rails, they store inside the cookie by default but include encryption+signing: https://guides.rubyonrails.org/security.html#session-storage. Rails does admit that this is something to argue about but it is the default.

I believe we should do the same by starting with storing inside the cookie both encrypted and signed, having that tower-sessions allows to switch session stores, it will allow users to switch store seamlessly, exactly like Rails.

schungx commented 4 months ago

One problem with storing session data on the client is that there is no way to force-invalidate a session, or to change permissions on the fly. What effort you save by having data stored on the client is negated by rigid inflexibility...

jondot commented 4 months ago

yes, that's completely true. lets draw it into a story:

  1. A loco developer starts out, generates a new app, uses the relevant defaults. they don't need to address any special session store, they get a cookie store by default, this is in development. so as little friction as possible
  2. The developer keeps building out their app, using the API for session store as needed. At this point they build the app and don't care about the mechanics of session or session store
  3. They now want to put their app to production. Our production YAML contains no defaults, and so, they will need to pick a secure, best practice configuration for every aspect of their app. In this case, they should pick a database-backed session store (tower-session supports 7-8 providers), and this should be as transparent as possible (say, they have a live Redis instance in prod, so they switch provider via configuration and only provide the Redis connection URL).

In step (3) the friction rises to the sky. But in steps (1), (2) it is smooth sailing and the best DX possible. What we can do as framework builders is make sure step (3) ("putting an app to production") is as automated and as easy as possible -- but still being a very mindful step, where a user should know exactly what configuration values they pick and how it affect security and performance. This can be done via documentation, or tooling, or both, and is definitely worth investing in -- but can also be separated into a different step for the evolution of Loco

schungx commented 3 months ago

IMHO, security is something that you either need it, or you don't. There are very few in-between where you need some security without much control. Most of the time those would be massive public API's where you don't really care much if they are used a bit more than you want.

So starting out with a simplistic view of security (meaning "not much") then progressing to production, a dev is suddenly faced with the horror that he hasn't thought out the important issues -- such as what permissions are there, where to set them, who can set them, where/how to set them (is there even a UI?), what to do when they are revoked, and what to do with malicious intent. And to do that without affecting much of the smooth UI that worked great when all those things were not a concern, Most of the time that means slapping on something that is held together by duck tape in the interest of time, because the friction in step (3) is insurmountable.

My suggestion to Loco is that it does it right from the beginning, with all the necessary features of a real production system. In Steps 1 and 2, simply omit the need for a store by saving the data on the client, but give a coherent API as if those things are actually persisted. Thus, in the end when the dev moves into production (step 3), he simply turns on a flag (session_store: true) and it magically "Just Works".

jondot commented 3 months ago

I agree about the security part, just also taking into account that using an encrypted and signed cookie store is not insecure, it is an option.

Looking at Rails argument for picking a default cookie store that is both encrypted and signed:

Rails CookieStore saves the session hash in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications and storage limitations of it:

Cookies have a size limit of 4 kB. Use cookies only for data which is relevant for the session.

Cookies are stored on the client-side. The client may preserve cookie contents even for expired cookies. The client may copy cookies to other machines. Avoid storing sensitive data in cookies.

Cookies are temporary by nature. The server can set expiration time for the cookie, but the client may delete the cookie and its contents before that. Persist all data that is of more permanent nature on the server side.

Session cookies do not invalidate themselves and can be maliciously reused. It may be a good idea to have your application invalidate old session cookies using a stored timestamp.

Rails encrypts cookies by default. The client cannot read or edit the contents of the cookie, without breaking encryption. If you take appropriate care of your secrets, you can consider your cookies to be generally secured.

The CookieStore uses the encrypted cookie jar to provide a secure, encrypted location to store session data. Cookie-based sessions thus provide both integrity as well as confidentiality to their contents. The encryption key, as well as the verification key used for signed cookies, is derived from the secret_key_base configuration value.

schungx commented 3 months ago

I agree about the security part, just also taking into account that using an encrypted and signed cookie store is not insecure, it is an option.

Totally agree. My suggestion is that, if it is an option, it should (as you said) have a clear automated way to convert into a "proper" production-ready scheme.

And that means that the dev should start dealing with the intricacies of proper security setup from day 1, even if he opts into use the cookie store option. By doing this, when moving into production no code needs to change when the permissions get relocated from client-side to the server.

That means, even for the cookie store option, Loco should have a standard set of API that works with permissions and updates the cookies (instead of updating the server store), leaving the dev with identical behavior regardless of the setup.

schungx commented 3 months ago

For example, a simple permissions API can work with a cookie store by queuing changes and resetting the cookie during the next request. Of course, after moving into a production sessions store, then it can simply update the store. The user sees no difference.

jondot commented 3 months ago

Yes, I think if we want, we could impose that cookie store cannot be used in release or in prod scenarios (easy to do), and by this way, to educate and force security-by-design. That seems like a good idea.

schungx commented 3 months ago

Yes, the user will complain, but he/she will know later on that it is literally a life-saver.

yinho999 commented 3 months ago

Yes, I think if we want, we could impose that cookie store cannot be used in release or in prod scenarios (easy to do), and by this way, to educate and force security-by-design. That seems like a good idea.

That is a great idea, I will start working on this.

schungx commented 3 months ago

I always wonder whether we can make it more automatic to configure permissions:

pub async fn patch_one(
    Path(id): Path<i32>,
    session: Session,
    State(ctx): State<AppContext>,
    Json(params): Json<PatchParams>,
) -> Result<Json<Model>> {
    // Must be allowed to update Entity with id
    // Similar to Rails' can? :read, item
    session.can:::<Entity>(UPDATE, id)?;

    // or...
    let Json(item) = load_item(id, &ctx.db).await?;
    session.can(UPDATE, item)?;

    if let Some(foo) = params.foo {
        // Check if the user has authority to update a particular field
        session.can::<Entity>(UPDATE_FIELD id, "foo")?;
    }
    ...
}