diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index a2e9bfede..4f2251fde 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -41,7 +41,7 @@ use lemmy_utils::{ }; use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation}; use serde::Deserialize; -use std::process::Command; +use std::{env, process::Command}; use url::Url; pub mod comment; @@ -100,16 +100,32 @@ pub(crate) async fn get_local_user_view_from_jwt( Ok(claims) => claims.claims, Err(_e) => return Err(ApiError::err("not_logged_in").into()), }; - let local_user_id = LocalUserId(claims.local_user_id); + let local_user_id = LocalUserId(claims.sub); let local_user_view = blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??; // Check for a site ban if local_user_view.person.banned { return Err(ApiError::err("site_ban").into()); } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + Ok(local_user_view) } +/// Checks if user's token was issued before user's password reset. +pub(crate) fn check_validator_time( + validator_time: &chrono::NaiveDateTime, + claims: &Claims, +) -> Result<(), LemmyError> { + let user_validation_time = validator_time.timestamp_millis() / 1000; + if user_validation_time > claims.iat { + Err(ApiError::err("not_logged_in").into()) + } else { + Ok(()) + } +} + pub(crate) async fn get_local_user_view_from_jwt_opt( jwt: &Option, pool: &DbPool, @@ -128,7 +144,7 @@ pub(crate) async fn get_local_user_settings_view_from_jwt( Ok(claims) => claims.claims, Err(_e) => return Err(ApiError::err("not_logged_in").into()), }; - let local_user_id = LocalUserId(claims.local_user_id); + let local_user_id = LocalUserId(claims.sub); let local_user_view = blocking(pool, move |conn| { LocalUserSettingsView::read(conn, local_user_id) }) @@ -137,6 +153,9 @@ pub(crate) async fn get_local_user_settings_view_from_jwt( if local_user_view.person.banned { return Err(ApiError::err("site_ban").into()); } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + Ok(local_user_view) } @@ -459,7 +478,11 @@ pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result Result { // Make a temp file path let uuid = uuid::Uuid::new_v4().to_string(); - let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid); + let file_path = format!( + "{}/lemmy_espeak_{}.wav", + env::temp_dir().to_string_lossy(), + &uuid + ); // Write the wav file Command::new("espeak") @@ -491,7 +514,70 @@ pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> { #[cfg(test)] mod tests { - use crate::captcha_espeak_wav_base64; + use crate::{captcha_espeak_wav_base64, check_validator_time}; + use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud}; + use lemmy_db_schema::source::{ + local_user::{LocalUser, LocalUserForm}, + person::{Person, PersonForm}, + }; + use lemmy_utils::claims::Claims; + + #[test] + fn test_should_not_validate_user_token_after_password_change() { + let conn = establish_unpooled_connection(); + + let new_person = PersonForm { + name: "Gerry9812".into(), + preferred_username: None, + avatar: None, + banner: None, + banned: None, + deleted: None, + published: None, + updated: None, + actor_id: None, + bio: None, + local: None, + private_key: None, + public_key: None, + last_refreshed_at: None, + inbox_url: None, + shared_inbox_url: None, + }; + + let inserted_person = Person::create(&conn, &new_person).unwrap(); + + let local_user_form = LocalUserForm { + person_id: inserted_person.id, + email: None, + matrix_user_id: None, + password_encrypted: "123456".to_string(), + admin: None, + show_nsfw: None, + theme: None, + default_sort_type: None, + default_listing_type: None, + lang: None, + show_avatars: None, + send_notifications_to_email: None, + }; + + let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap(); + + let jwt = Claims::jwt(inserted_local_user.id.0).unwrap(); + let claims = Claims::decode(&jwt).unwrap().claims; + let check = check_validator_time(&inserted_local_user.validator_time, &claims); + assert!(check.is_ok()); + + // The check should fail, since the validator time is now newer than the jwt issue time + let updated_local_user = + LocalUser::update_password(&conn, inserted_local_user.id, &"password111").unwrap(); + let check_after = check_validator_time(&updated_local_user.validator_time, &claims); + assert!(check_after.is_err()); + + let num_deleted = Person::delete(&conn, inserted_person.id).unwrap(); + assert_eq!(1, num_deleted); + } #[test] fn test_espeak() { diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index e9daa8196..266d28eec 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -130,7 +130,7 @@ impl Perform for Login { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt(local_user_view.local_user.id.0, Settings::get().hostname())?, + jwt: Claims::jwt(local_user_view.local_user.id.0)?, }) } } @@ -336,7 +336,7 @@ impl Perform for Register { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt(inserted_local_user.id.0, Settings::get().hostname())?, + jwt: Claims::jwt(inserted_local_user.id.0)?, }) } } @@ -526,7 +526,7 @@ impl Perform for SaveUserSettings { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt(updated_local_user.id.0, Settings::get().hostname())?, + jwt: Claims::jwt(updated_local_user.id.0)?, }) } } @@ -1078,7 +1078,7 @@ impl Perform for PasswordChange { // Return the jwt Ok(LoginResponse { - jwt: Claims::jwt(updated_local_user.id.0, Settings::get().hostname())?, + jwt: Claims::jwt(updated_local_user.id.0)?, }) } } diff --git a/crates/db_queries/src/source/local_user.rs b/crates/db_queries/src/source/local_user.rs index cd93d3fb1..eabd067d3 100644 --- a/crates/db_queries/src/source/local_user.rs +++ b/crates/db_queries/src/source/local_user.rs @@ -2,26 +2,13 @@ use crate::Crud; use bcrypt::{hash, DEFAULT_COST}; use diesel::{dsl::*, result::Error, *}; use lemmy_db_schema::{ + naive_now, schema::local_user::dsl::*, source::local_user::{LocalUser, LocalUserForm}, LocalUserId, PersonId, }; -mod safe_type { - use crate::ToSafe; - use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser}; - - type Columns = (id, person_id, admin, matrix_user_id); - - impl ToSafe for LocalUser { - type SafeColumns = Columns; - fn safe_columns_tuple() -> Self::SafeColumns { - (id, person_id, admin, matrix_user_id) - } - } -} - mod safe_settings_type { use crate::ToSafeSettings; use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser}; @@ -39,6 +26,7 @@ mod safe_settings_type { show_avatars, send_notifications_to_email, matrix_user_id, + validator_time, ); impl ToSafeSettings for LocalUser { @@ -59,6 +47,7 @@ mod safe_settings_type { show_avatars, send_notifications_to_email, matrix_user_id, + validator_time, ) } } @@ -92,7 +81,10 @@ impl LocalUser_ for LocalUser { let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password"); diesel::update(local_user.find(local_user_id)) - .set((password_encrypted.eq(password_hash),)) + .set(( + password_encrypted.eq(password_hash), + validator_time.eq(naive_now()), + )) .get_result::(conn) } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index c5bf7d2fe..9bb3fe2d6 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -155,6 +155,7 @@ table! { show_avatars -> Bool, send_notifications_to_email -> Bool, matrix_user_id -> Nullable, + validator_time -> Timestamp, } } diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs index 750a2255c..11dac6c9c 100644 --- a/crates/db_schema/src/source/local_user.rs +++ b/crates/db_schema/src/source/local_user.rs @@ -17,6 +17,7 @@ pub struct LocalUser { pub show_avatars: bool, pub send_notifications_to_email: bool, pub matrix_user_id: Option, + pub validator_time: chrono::NaiveDateTime, } // TODO redo these, check table defaults @@ -53,4 +54,5 @@ pub struct LocalUserSettings { pub show_avatars: bool, pub send_notifications_to_email: bool, pub matrix_user_id: Option, + pub validator_time: chrono::NaiveDateTime, } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 47aca46fd..6fc370ed3 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -227,7 +227,7 @@ fn get_feed_front( jwt: String, ) -> Result { let site_view = SiteView::read(&conn)?; - let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.local_user_id); + let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); let person_id = LocalUser::read(&conn, local_user_id)?.person_id; let posts = PostQueryBuilder::create(&conn) @@ -254,7 +254,7 @@ fn get_feed_front( fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result { let site_view = SiteView::read(&conn)?; - let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.local_user_id); + let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); let person_id = LocalUser::read(&conn, local_user_id)?.person_id; let sort = SortType::New; diff --git a/crates/utils/src/claims.rs b/crates/utils/src/claims.rs index e84f34f91..fc3c43579 100644 --- a/crates/utils/src/claims.rs +++ b/crates/utils/src/claims.rs @@ -6,8 +6,11 @@ type Jwt = String; #[derive(Debug, Serialize, Deserialize)] pub struct Claims { - pub local_user_id: i32, + /// local_user_id, standard claim by RFC 7519. + pub sub: i32, pub iss: String, + /// Time when this token was issued as UNIX-timestamp in seconds + pub iat: i64, } impl Claims { @@ -23,10 +26,11 @@ impl Claims { ) } - pub fn jwt(local_user_id: i32, hostname: String) -> Result { + pub fn jwt(local_user_id: i32) -> Result { let my_claims = Claims { - local_user_id, - iss: hostname, + sub: local_user_id, + iss: Settings::get().hostname(), + iat: chrono::Utc::now().timestamp_millis() / 1000, }; encode( &Header::default(), diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index 3911a18f9..d64884bfc 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -24,7 +24,7 @@ static CONFIG_FILE: &str = "config/config.hjson"; lazy_static! { static ref SETTINGS: RwLock = RwLock::new(match Settings::init() { Ok(c) => c, - Err(e) => panic!("{}", e), + Err(_) => Settings::default(), }); } diff --git a/migrations/2021-03-19-014144_add_col_local_user_validator_time/down.sql b/migrations/2021-03-19-014144_add_col_local_user_validator_time/down.sql new file mode 100644 index 000000000..657bfb3d4 --- /dev/null +++ b/migrations/2021-03-19-014144_add_col_local_user_validator_time/down.sql @@ -0,0 +1 @@ +alter table local_user drop column validator_time; diff --git a/migrations/2021-03-19-014144_add_col_local_user_validator_time/up.sql b/migrations/2021-03-19-014144_add_col_local_user_validator_time/up.sql new file mode 100644 index 000000000..4fbb5eb91 --- /dev/null +++ b/migrations/2021-03-19-014144_add_col_local_user_validator_time/up.sql @@ -0,0 +1 @@ +alter table local_user add column validator_time timestamp not null default now();