Merge branch '1462-jwt-revocation-on-pwd-change' of https://github.com/Mart-Bogdan/lemmy into Mart-Bogdan-1462-jwt-revocation-on-pwd-change
This commit is contained in:
commit
360d4ea8d1
8 changed files with 144 additions and 3 deletions
|
@ -36,7 +36,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;
|
||||
|
@ -98,6 +98,11 @@ pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_,
|
|||
if user.banned {
|
||||
return Err(ApiError::err("site_ban").into());
|
||||
}
|
||||
// if user's token was issued before user's password reset.
|
||||
let user_validation_time = user.validator_time.timestamp_millis() / 1000;
|
||||
if user_validation_time > claims.iat {
|
||||
return Err(ApiError::err("not_logged_in").into());
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
|
@ -125,6 +130,11 @@ pub(crate) async fn get_user_safe_settings_from_jwt(
|
|||
if user.banned {
|
||||
return Err(ApiError::err("site_ban").into());
|
||||
}
|
||||
// if user's token was issued before user's password reset.
|
||||
let user_validation_time = user.validator_time.timestamp_millis() / 1000;
|
||||
if user_validation_time > claims.iat {
|
||||
return Err(ApiError::err("not_logged_in").into());
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
|
@ -444,7 +454,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")
|
||||
|
@ -476,7 +490,90 @@ 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, get_user_from_jwt, get_user_safe_settings_from_jwt};
|
||||
use lemmy_db_queries::{
|
||||
establish_pooled_connection,
|
||||
source::user::User,
|
||||
Crud,
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
use lemmy_db_schema::source::user::{UserForm, User_};
|
||||
use lemmy_utils::claims::Claims;
|
||||
use std::{
|
||||
env::{current_dir, set_current_dir},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_should_not_validate_user_token_after_password_change() {
|
||||
struct CwdGuard(PathBuf);
|
||||
impl Drop for CwdGuard {
|
||||
fn drop(&mut self) {
|
||||
let _ = set_current_dir(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
let _dir_bkp = CwdGuard(current_dir().unwrap());
|
||||
|
||||
// so configs could be read
|
||||
let _ = set_current_dir("../..");
|
||||
|
||||
let conn = establish_pooled_connection();
|
||||
|
||||
let new_user = UserForm {
|
||||
name: "user_df342sgf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: Some(false),
|
||||
published: None,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "browser".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
actor_id: None,
|
||||
bio: None,
|
||||
local: true,
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
inbox_url: None,
|
||||
shared_inbox_url: None,
|
||||
};
|
||||
|
||||
let inserted_user: User_ = User_::create(&conn.get().unwrap(), &new_user).unwrap();
|
||||
|
||||
let jwt_token = Claims::jwt(inserted_user.id, String::from("my-host.com")).unwrap();
|
||||
|
||||
get_user_from_jwt(&jwt_token, &conn)
|
||||
.await
|
||||
.expect("User should be decoded");
|
||||
|
||||
get_user_safe_settings_from_jwt(&jwt_token, &conn)
|
||||
.await
|
||||
.expect("User should be decoded");
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
|
||||
User_::update_password(&conn.get().unwrap(), inserted_user.id, &"password111").unwrap();
|
||||
|
||||
get_user_from_jwt(&jwt_token, &conn)
|
||||
.await
|
||||
.expect_err("JWT decode should fail after password change");
|
||||
|
||||
get_user_safe_settings_from_jwt(&jwt_token, &conn)
|
||||
.await
|
||||
.expect_err("JWT decode should fail after password change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_espeak() {
|
||||
|
|
|
@ -251,6 +251,33 @@ pub fn establish_unpooled_connection() -> PgConnection {
|
|||
conn
|
||||
}
|
||||
|
||||
pub fn establish_pooled_connection(
|
||||
) -> diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>> {
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
|
||||
// Set up the r2d2 connection pool
|
||||
let db_url = match get_database_url_from_env() {
|
||||
Ok(url) => url,
|
||||
Err(e) => panic!(
|
||||
"Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
|
||||
e
|
||||
),
|
||||
};
|
||||
|
||||
let manager = ConnectionManager::<PgConnection>::new(&db_url);
|
||||
let pool = Pool::builder()
|
||||
.max_size(1)
|
||||
.build(manager)
|
||||
.unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
|
||||
|
||||
let conn = pool.get().unwrap();
|
||||
|
||||
// Run the migrations from code
|
||||
embedded_migrations::run(&conn).unwrap();
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_REGEX: Regex =
|
||||
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
|
||||
|
|
|
@ -173,6 +173,7 @@ mod safe_settings_type {
|
|||
last_refreshed_at,
|
||||
banner,
|
||||
deleted,
|
||||
validator_time,
|
||||
);
|
||||
|
||||
impl ToSafeSettings for User_ {
|
||||
|
@ -202,6 +203,7 @@ mod safe_settings_type {
|
|||
last_refreshed_at,
|
||||
banner,
|
||||
deleted,
|
||||
validator_time,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -296,6 +298,7 @@ impl User for User_ {
|
|||
.set((
|
||||
password_encrypted.eq(password_hash),
|
||||
updated.eq(naive_now()),
|
||||
validator_time.eq(naive_now()),
|
||||
))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
@ -446,6 +449,7 @@ mod tests {
|
|||
deleted: false,
|
||||
inbox_url: inserted_user.inbox_url.to_owned(),
|
||||
shared_inbox_url: None,
|
||||
validator_time: inserted_user.published,
|
||||
};
|
||||
|
||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||
|
|
|
@ -408,6 +408,7 @@ table! {
|
|||
deleted -> Bool,
|
||||
inbox_url -> Text,
|
||||
shared_inbox_url -> Nullable<Text>,
|
||||
validator_time -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ pub struct User_ {
|
|||
pub deleted: bool,
|
||||
pub inbox_url: DbUrl,
|
||||
pub shared_inbox_url: Option<DbUrl>,
|
||||
pub validator_time: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
/// A safe representation of user, without the sensitive info
|
||||
|
@ -86,6 +87,7 @@ pub struct UserSafeSettings {
|
|||
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||
pub banner: Option<DbUrl>,
|
||||
pub deleted: bool,
|
||||
pub validator_time: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
|
||||
|
|
|
@ -6,8 +6,14 @@ type Jwt = String;
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
/// User id, for backward compatibility with client apps.
|
||||
/// Claim [sub](Claims::sub) is used in server-side checks.
|
||||
pub id: i32,
|
||||
/// 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 {
|
||||
|
@ -26,7 +32,9 @@ impl Claims {
|
|||
pub fn jwt(user_id: i32, hostname: String) -> Result<Jwt, jsonwebtoken::errors::Error> {
|
||||
let my_claims = Claims {
|
||||
id: user_id,
|
||||
sub: user_id,
|
||||
iss: hostname,
|
||||
iat: chrono::Utc::now().timestamp_millis() / 1000,
|
||||
};
|
||||
encode(
|
||||
&Header::default(),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE user_ DROP COLUMN validator_time;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE user_ ADD COLUMN validator_time timestamp not null default now();
|
Loading…
Reference in a new issue