Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
chrono = { version = "0.4", default-features = false, features = ["serde"] }
cookie = "0.18"
diesel = { version = ">=2, <2.2", features = ["serde_json"] }
serde_urlencoded = "0.7"
include_dir = "0.7"
Expand All @@ -43,6 +44,7 @@ uuid = { version = "1.8", default-features = false, features = ["v8"] }
ft-sys-shared = { path = "ft-sys-shared", version = "0.1.2" }
ft-derive = { path = "ft-derive", version = "0.1.1" }
ft-sys = { path = "ft-sys", version = "0.1.3" }
ft-sdk = { path = "ft-sdk", version = "0.1.12" }


[workspace.dependencies.rusqlite]
Expand Down
1 change: 1 addition & 0 deletions ft-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ diesel = { workspace = true, optional = true }
include_dir.workspace = true
rand_core.workspace = true
uuid.workspace = true
cookie.workspace = true

[dev-dependencies]
pretty_assertions = "1"
83 changes: 68 additions & 15 deletions ft-sdk/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ mod utils;

pub use ft_sys_shared::SESSION_KEY;
pub use schema::{fastn_session, fastn_user};
pub use session::SessionID;
#[cfg(feature = "auth-provider")]
pub use session::{expire_session_cookie, set_session_cookie};
pub use session::{SessionID, SessionIDError};
pub use utils::{user_data_by_query, Counter};

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -73,29 +75,52 @@ pub fn session_providers() -> Vec<String> {
todo!()
}

/// Fetches user data based on a given session cookie.
///
/// This function fetches user data based on a given session cookie if the
/// session cookie is found and is valid and user data is found.
/// If the session cookie not found, it returns `None`.
#[cfg(feature = "field-extractors")]
pub fn ud(
cookie: ft_sdk::Cookie<SESSION_KEY>,
conn: &mut ft_sdk::Connection,
) -> Result<Option<ft_sys::UserData>, UserDataError> {
if let Some(v) = ft_sys::env::var("DEBUG_LOGGED_IN".to_string()) {
let mut v = v.splitn(4, ' ');
return Ok(Some(ft_sys::UserData {
id: v.next().unwrap().parse().unwrap(),
identity: v.next().unwrap_or_default().to_string(),
name: v.next().map(|v| v.to_string()).unwrap_or_default(),
email: v.next().map(|v| v.to_string()).unwrap_or_default(),
verified_email: true,
}));
ft_sdk::println!("session cookie: {cookie}");
// Check if debug user data is available, return it if found.
if let Some(ud) = get_debug_ud() {
return Ok(Some(ud));
}

ft_sdk::println!("sid: {cookie}");

let sid = match cookie.0 {
// Extract the session ID from the cookie.
let session_id = match cookie.0 {
Some(v) => v,
None => return Ok(None),
};

ud_from_session_key(conn, &ft_sdk::auth::SessionID(session_id))
}

/// Fetches user data based on a given session id.
///
/// This function fetches user data based on a given session id if the
/// session is valid and user data is found.
/// If the session cookie not found, it returns `None`.
#[cfg(feature = "field-extractors")]
pub fn ud_from_session_key(
conn: &mut ft_sdk::Connection,
session_id: &ft_sdk::auth::SessionID,
) -> Result<Option<ft_sys::UserData>, UserDataError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we should have two method. Merge the two ud related methods please.

Copy link
Contributor Author

@Arpita-Jaiswal Arpita-Jaiswal Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are not session_id passing in cookie but in Authorization header. We need to have other function to fetch ud from Authorization header. This we can do by having something similar to ft_sdk::Cookie with FromRequest implemented which I guess I am going to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the original ud() method to accept SessionID instead of Cookie<"session-id">. Then you won't need this duplication.

// Check if debug user data is available, return it if found.
if let Some(ud) = get_debug_ud() {
return Ok(Some(ud));
}

ft_sdk::println!("sid: {}", session_id.0);

// Validate the session using the extracted session ID.
session_id.validate_session(conn)?;

// Query the database to get the user data associated with the session ID.
let (UserId(id), identity, data) = match utils::user_data_by_query(
conn,
r#"
Expand All @@ -107,13 +132,14 @@ pub fn ud(
fastn_session.id = $1
AND fastn_user.id = fastn_session.uid
"#,
sid.as_str(),
session_id.0.as_str(),
) {
Ok(v) => v,
Err(UserDataError::NoDataFound) => return Ok(None),
Err(e) => return Err(e),
};

// Extract the primary email from the user data, prefer verified emails.
let email = data
.verified_emails
.first()
Expand All @@ -122,19 +148,46 @@ pub fn ud(

Ok(Some(ft_sys::UserData {
id,
identity: identity.expect("user fetched from session cookie must have identity"),
identity: identity.ok_or_else(|| UserDataError::IdentityNotFound)?,
name: data.name.unwrap_or_default(),
email,
verified_email: !data.verified_emails.is_empty(),
}))
}

/// Check if debug user data is available, return it if found.
fn get_debug_ud() -> Option<ft_sys::UserData> {
match ft_sys::env::var("DEBUG_LOGGED_IN".to_string()) {
Some(debug_logged_in) => {
let mut debug_logged_in = debug_logged_in.splitn(4, ' ');
Some(ft_sys::UserData {
id: debug_logged_in.next().unwrap().parse().unwrap(),
identity: debug_logged_in.next().unwrap_or_default().to_string(),
name: debug_logged_in
.next()
.map(|v| v.to_string())
.unwrap_or_default(),
email: debug_logged_in
.next()
.map(|v| v.to_string())
.unwrap_or_default(),
verified_email: true,
})
}
None => None,
}
}

#[derive(Debug, thiserror::Error)]
pub enum UserDataError {
#[error("no data found for the provider")]
NoDataFound,
#[error("multiple rows found")]
MultipleRowsFound,
#[error("session error: {0:?}")]
SessionError(#[from] ft_sdk::auth::SessionIDError),
#[error("user fetched from session cookie must have identity")]
IdentityNotFound,
#[error("db error: {0:?}")]
DatabaseError(#[from] diesel::result::Error),
#[error("failed to deserialize data from db: {0:?}")]
Expand Down
55 changes: 44 additions & 11 deletions ft-sdk/src/auth/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,23 +290,57 @@ pub fn create_user(
Ok(ft_sdk::auth::UserId(user_id))
}

/// persist the user in session and redirect to `next`
/// Logs in a user and manages session creation or update.
///
/// `identity`: Eg for GitHub, it could be the username. This is stored in the cookie so can be
/// retrieved without a db call to show a user identifiable information.
/// # Arguments
///
/// * `conn` - Mutable reference to a `ft_sdk::Connection` to interact with the database.
/// * `user_id` - Reference to a `ft_sdk::UserId` representing the user's ID.
/// * `session_id` - Optional `ft_sdk::auth::SessionID` representing an existing session ID.
///
/// # Returns
///
/// A `Result` containing a `ft_sdk::auth::SessionID` if the login operation is successful,
/// or a `LoginError` if there's an issue with the login process.
pub fn login(
conn: &mut ft_sdk::Connection,
ft_sdk::UserId(user_id): &ft_sdk::UserId,
user_id: &ft_sdk::UserId,
session_id: Option<ft_sdk::auth::SessionID>,
) -> Result<ft_sdk::auth::SessionID, LoginError> {
login_with_custom_session_expiration(conn, user_id, session_id, None)
}

/// Logs in a user with customizable session expiration and manages session creation or update.
///
/// # Arguments
///
/// * `conn` - Mutable reference to a `ft_sdk::Connection` to interact with the database.
/// * `user_id` - Reference to a `ft_sdk::UserId` representing the user's ID.
/// * `session_id` - Optional `ft_sdk::auth::SessionID` representing an existing session ID.
/// * `session_expiration_duration` - Optional `chrono::Duration` for custom session expiration.
///
/// # Returns
///
/// A `Result` containing a `ft_sdk::auth::SessionID` if the login operation is successful,
/// or a `LoginError` if there's an issue with the login process.
pub fn login_with_custom_session_expiration(
conn: &mut ft_sdk::Connection,
user_id: &ft_sdk::UserId,
session_id: Option<ft_sdk::auth::SessionID>,
session_expiration_duration: Option<chrono::Duration>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a global configuration concern. A site will have a single global expiration. Use environment variable to get its value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ft_sdk::env::var doesn't work on hostn.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it not work? If it does not work we will make it work. Write the code with the assumtion that the functions we have provided in sdk work.

) -> Result<ft_sdk::auth::SessionID, LoginError> {
match session_id {
Some(session_id) if session_id.0 == "hello" => {
Ok(ft_sdk::auth::session::create_with_user(conn, *user_id)?)
}
Some(session_id) => Ok(ft_sdk::auth::session::set_user_id(
conn, session_id, *user_id,
Some(session_id) if session_id.0 == "hello" => Ok(ft_sdk::auth::session::create_with_user(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove this hello hack now. I think we did it because we had some wrong cookie set with this value which we were not able to delete.

conn,
user_id,
session_expiration_duration,
)?),
_ => Ok(ft_sdk::auth::session::set_user_id(
conn,
session_id,
user_id,
session_expiration_duration,
)?),
None => Ok(ft_sdk::auth::session::create_with_user(conn, *user_id)?),
}
}

Expand All @@ -316,7 +350,6 @@ pub enum LoginError {
DatabaseError(#[from] diesel::result::Error),
#[error("set user id for session {0}")]
SetUserIDError(#[from] ft_sdk::auth::session::SetUserIDError),

#[error("json error: {0}")]
JsonError(#[from] serde_json::Error),
}
Expand Down
1 change: 1 addition & 0 deletions ft-sdk/src/auth/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ diesel::table! {
data -> Text,
updated_at -> Timestamptz,
created_at -> Timestamptz,
expires_at -> Nullable<Timestamptz>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure it should be nullable. Lets make every session expire after certain timeout. And we can then add a function which will extend the expires_at by 30 days every time we access it. Also shouldnt tracking expiry by cookie expiry be enough, why do we have to store this in DB?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cookie will surely delete it but how are we going to delete this from db?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A background worker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a background worker should read from db when the session should expire and then delete it. If my understanding is correct then we need expires_at column in the fastn_session table.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, yes. But since there are multiple possible behaviours for session expiry, let's not tackle session expiry related stuff now. First let's have a discussion and full design on session expiry then we write code.

}
}

Expand Down
Loading