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

New Session For Every Request #53

Closed
BeaconBrigade opened this issue Feb 9, 2023 · 15 comments
Closed

New Session For Every Request #53

BeaconBrigade opened this issue Feb 9, 2023 · 15 comments

Comments

@BeaconBrigade
Copy link

I'm trying to implement a /login and /logout routes as well as some protected routes. However, after logging in, it seems like the log in isn't saved. When I ping /login it adds the user to a session, but by the time I try to access a protected route, the session and user are gone. I got the examples to work, but my repo based off of them just doesn't. Every protected route always returns forbidden and even the logout route says the current_user is None.

I inserted tracing everywhere to figure out what was happening and noticed that between each request a new session is created. I don't know if this is what's supposed to happen, but all of the data stored in the session is lost.

My SessionLayer is built with a MemoryStore and my AuthLayer is built with an SqliteStore. I'm using sqlx and sqlite as a database. I made sure my users are stored in a table called users in my database.

If it helps, here's my setup for the layers:

let session_store = MemoryStore::new();
let session_layer = SessionLayer::new(session_store, &config.session_secret);

let user_store =
    SqliteStore::<User, Role>::new(SqlitePool::connect(&config.database_file).await?);
let auth_layer = AuthLayer::new(user_store, &config.session_secret);

config.session_secret is a 64 byte array, and config.database_file is the path to my sqlite db.

type RequireAuth = RequireAuthorizationLayer<User, Role>;

let app = Router::new()
    .route(
        "/lists",
        get(handlers::fetch_user_lists).layer(RequireAuth::login()),
    )
    .route(
        "/list",
        get(handlers::fetch_list).layer(RequireAuth::login()),
    )
    .route(
        "/get-lists",
        get(handlers::get_lists).layer(RequireAuth::login_with_role(Role::Admin..)),
    )
    .route(
        "/get-users",
        get(handlers::get_users).layer(RequireAuth::login_with_role(Role::Admin..)),
    )
    .route("/status", get(|| async { StatusCode::OK }))
    .route("/login", get(handlers::login))
    .route("/logout", get(handlers::logout))
    .layer(auth_layer)
    .layer(session_layer)
    .with_state(app_state);

The app_state contains a sqlx pool for the database. The enum Roletype looks like:

#[derive(
    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, sqlx::Type,
)]
#[repr(i32)]
pub enum Role {
    /// Regular user access
    #[default]
    User = 0,
    /// Full access to the database
    Admin = 1,
}

I'm new to axum-login, so sorry for the information dump, I just wanted to put anything that might be useful.

The /login route takes a json body which describes the user to log into, a name and password. The handler looks like this:

pub type AuthContext = axum_login::extractors::AuthContext<User, SqliteStore<User, Role>, Role>;

#[debug_handler]
pub async fn login(
    State(app): State<AppState>,
    mut auth: AuthContext,
    Json(user): Json<LoginUser>,
) -> AppResult<()> {
    debug!("logging in user: {user:?}");
    let user: User = sqlx::query_as("SELECT * FROM users WHERE name = ? AND password_hash = ?")
        .bind(&user.name)
        .bind(&user.password_hash)
        .fetch_one(&mut app.conn.acquire().await?)
        .await?;

    info!("user: {user:?}");

    auth.login(&user).await?;

    Ok(())
}

Inside the /login handler I can check auth.current_user and it is set correctly, but as soon as another route is called that is gone. For example on one run of the server:

Directly before calling auth.login(&user).await?

Session { 
    id: "jGomHmRqscfQGcLIpZPLzQvN8lDqlQ5qQJMA0GHlamQ=", 
    expiry: Some(2023-02-10T02:20:06.301437200Z), 
    data: RwLock { 
        data: {}, 
        poisoned: false, 
        .. 
    }, 
    cookie_value: Some("A/MJXP4FMV+rWiRmZMg0X5frpm9Gzt8dqSBCDW/YdVwroQTlmyocXT+X5FnMM5te4Y4PMnKmUwJT5kpBCXa+oQ=="), 
    data_changed: false, 
    destroy: false 
}

Directly after:

Session { 
    id: "jGomHmRqscfQGcLIpZPLzQvN8lDqlQ5qQJMA0GHlamQ=", 
    expiry: Some(2023-02-10T02:20:06.301437200Z), 
    data: RwLock { 
        data: {
            "_user_id": "\"3\"", 
            "_auth_id": "\"0qL/ZztGgEixRWFrMorDFNUsauceo4vmDcHcohY18PLWUUxTVc0/0SSSYMYeAaut45ZgEXbPJa1xeQBpyKP86w==\""
        }, 
        poisoned: false, 
        .. 
    }, 
    cookie_value: Some("A/MJXP4FMV+rWiRmZMg0X5frpm9Gzt8dqSBCDW/YdVwroQTlmyocXT+X5FnMM5te4Y4PMnKmUwJT5kpBCXa+oQ=="), 
    data_changed: true, 
    destroy: false 
}

You can see that the user_id has been added to the session. But, on the next request to /lists (or any route, it's all the same) we have:

Session { 
    id: "txK5yTK9y5Hyu6wxcSF6SwK079pmdSI7n/YV7RPg/HQ=", 
    expiry: Some(2023-02-10T02:20:13.404553800Z), 
    data: RwLock { 
        data: {}, 
        poisoned: false,
        ..
    }, 
    cookie_value: Some("29tPyCptmKMlBSJ8D08lYs1zFdz7NXaHouUiSt6u1o9JZvCI/OzmvZntzUSB6iMqFfI37YCkmKqZaKjQ8DsXjg=="), 
    data_changed: false, 
    destroy: false 
}

All of a sudden we have a brand new session with the old data gone. I've been struggling for hours trying to find out what I'm doing wrong, and at this point I'm pretty sure it's just some tiny thing I'm missing. Or of course, I'm just misunderstanding how I should be using the library. If so, please let me know. I'd really appreciate if anyone could take a look and see if they notice anything out of order. Also, if you want more information just let me know. Thanks in advance

@maxcountryman
Copy link
Owner

Have you tried toggling secure? Note that the examples disable secure because they assume you'll be running locally. By default it's enabled.

@BeaconBrigade
Copy link
Author

Yes. I tried with_secure on the session layer, but that didn't change what happened. I still got new sessions for every request

@maxcountryman
Copy link
Owner

Next try same site.

Getting a new session implies the client is not sending the cookie with the request.

That's likely because of the cookie settings. But you should also inspect the client to see what's happening there.

@BeaconBrigade
Copy link
Author

SameSite had no effect, but by changing the /login route to POST, I was able to access it using the fetch api in Firefox dev tools. However this didn't work in chrome because of CORS errors. After fixing the CORS errors, chrome ran into the original problem. On Firefox the sessions are reused, and upon inspecting requests, I can see that a cookie header is set on requests from firefox, but chrome doesn't set the session cookie.

Every response to chrome contains a set-cookie header. I'm assuming this is because chrome isn't sending the cookie in the first place. Any ideas why chrome might not send the cookie? SameSite is Strict and secure is true

@maxcountryman
Copy link
Owner

You've identified the issue: your client is not sending the cookie.

To fix this you'll have to dig into what cookie settings are necessary to ensure the client sends the cookies.

For dev I would suggest disabling secure and using lax same site.

MDN has good documentation around these topics that might help.

@BeaconBrigade
Copy link
Author

Alright thanks for your help!

@ejmg
Copy link

ejmg commented Feb 9, 2023

Not to revive a dead thread that was just resolved, but I am incidentally hitting this precise same issue on Chrome. If you figure out the issue, can you let us know, @BeaconBrigade? At worst, other people can find this thread if the issue is common enough.

@maxcountryman
Copy link
Owner

@ejmg please try what I suggested above. These issues relate to how clients (in this case Chrome) will transmit cookies. Cookies need to be configured such that the client will transmit them as expected. The underlying crate for managing sessions exposes an interface for configuring these.

@ejmg
Copy link

ejmg commented Feb 10, 2023

Turns out, my issue was of the same nature that was discussed in this discussion thread: #45 (reply in thread)

@maxcountryman
Copy link
Owner

@ejmg curious in what way? You weren't expecting axum-login to the query the user store?

@ejmg
Copy link

ejmg commented Feb 10, 2023

I mistook the nature/function of axum_login. I thought that operations like auth.login(&user)... (where auth is some AuthContext<T...> value) also updated the session_store that is passed on to the router.

@maxcountryman
Copy link
Owner

@ejmg are you trying to access the the user value in the session directly? I think I'm confused about what you ran into here.

@ejmg
Copy link

ejmg commented Feb 10, 2023

using the simple-with-roles example as a reference, I mistakenly thought that axum_login automatically updated the users in the user_store that we pass to the AuthLayer struct.

With the setup:

    let session_store = SessionMemoryStore::new();
    let session_layer = SessionLayer::new(session_store, &secret).with_secure(false);

    let store = Arc::new(RwLock::new(HashMap::default())); // [1]
    let user = User::get_rusty_user();

    store.write().await.insert(user.get_id(), user);

    let user_store = AuthMemoryStore::new(&store);
    let auth_layer = AuthLayer::new(user_store, &secret);

and the route:

    async fn login_handler(mut auth: AuthContext) {
        auth.login(&User::get_rusty_user()).await.unwrap(); 
    }

I mistakingly thought that whatever arbitrary user we passed on to auth.login(&some_user).await, axum_login added this user to the original user store [1]. In the case of the example code, the User value returned from get_rusty_user() is already populated in [1] whereas I wasn't doing this with my own project. axum_login was correctly generating the cookie sessions and putting them in my browser and the browser was correctly using them; I simply didn't have any users in my original store, so when I hit a route that was protected, eg:

    async fn protected_handler(Extension(user): Extension<User>) -> impl IntoResponse {
        format!("Logged in as: {}", user.name)
    }

I received a 401 error instead.

@maxcountryman
Copy link
Owner

@ejmg ah okay, thanks for the detailed explanation. I think I understand now. The confusion is that axum-login doesn't take an updated user and write it to the store. In other words it doesn't manage the state of the user store in a write fashion only in a read fashion. Is there something we could do in the docs or examples to make that more obvious?

@ejmg
Copy link

ejmg commented Feb 10, 2023

The misunderstanding is, in my case, definitely one of overall experience and conflating different concepts. At minimum, it might be good to explicitly comment that the user store needs to be pre-populated or, in the case of using the same application DB for users, that you can use the same DB for the user store when configured correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants