Merge branch 'Mart-Bogdan-1462-jwt-revocation-on-pwd-change' into jwt_revocation_dess
This commit is contained in:
commit
05b485b678
10 changed files with 118 additions and 31 deletions
|
@ -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<String>,
|
||||
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<String, LemmyEr
|
|||
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
|
||||
// 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() {
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<Self>(conn)
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,7 @@ table! {
|
|||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
validator_time -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ pub struct LocalUser {
|
|||
pub show_avatars: bool,
|
||||
pub send_notifications_to_email: bool,
|
||||
pub matrix_user_id: Option<String>,
|
||||
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<String>,
|
||||
pub validator_time: chrono::NaiveDateTime,
|
||||
}
|
||||
|
|
|
@ -227,7 +227,7 @@ fn get_feed_front(
|
|||
jwt: String,
|
||||
) -> Result<ChannelBuilder, LemmyError> {
|
||||
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<ChannelBuilder, LemmyError> {
|
||||
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;
|
||||
|
|
|
@ -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<Jwt, jsonwebtoken::errors::Error> {
|
||||
pub fn jwt(local_user_id: i32) -> Result<Jwt, jsonwebtoken::errors::Error> {
|
||||
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(),
|
||||
|
|
|
@ -24,7 +24,7 @@ static CONFIG_FILE: &str = "config/config.hjson";
|
|||
lazy_static! {
|
||||
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("{}", e),
|
||||
Err(_) => Settings::default(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
alter table local_user drop column validator_time;
|
|
@ -0,0 +1 @@
|
|||
alter table local_user add column validator_time timestamp not null default now();
|
Loading…
Reference in a new issue