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 lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::process::Command;
|
use std::{env, process::Command};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
|
@ -98,6 +98,11 @@ pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_,
|
||||||
if user.banned {
|
if user.banned {
|
||||||
return Err(ApiError::err("site_ban").into());
|
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)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +130,11 @@ pub(crate) async fn get_user_safe_settings_from_jwt(
|
||||||
if user.banned {
|
if user.banned {
|
||||||
return Err(ApiError::err("site_ban").into());
|
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)
|
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> {
|
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
|
||||||
// Make a temp file path
|
// Make a temp file path
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
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
|
// Write the wav file
|
||||||
Command::new("espeak")
|
Command::new("espeak")
|
||||||
|
@ -476,7 +490,90 @@ pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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]
|
#[test]
|
||||||
fn test_espeak() {
|
fn test_espeak() {
|
||||||
|
|
|
@ -251,6 +251,33 @@ pub fn establish_unpooled_connection() -> PgConnection {
|
||||||
conn
|
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! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex =
|
static ref EMAIL_REGEX: Regex =
|
||||||
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
|
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,
|
last_refreshed_at,
|
||||||
banner,
|
banner,
|
||||||
deleted,
|
deleted,
|
||||||
|
validator_time,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl ToSafeSettings for User_ {
|
impl ToSafeSettings for User_ {
|
||||||
|
@ -202,6 +203,7 @@ mod safe_settings_type {
|
||||||
last_refreshed_at,
|
last_refreshed_at,
|
||||||
banner,
|
banner,
|
||||||
deleted,
|
deleted,
|
||||||
|
validator_time,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,6 +298,7 @@ impl User for User_ {
|
||||||
.set((
|
.set((
|
||||||
password_encrypted.eq(password_hash),
|
password_encrypted.eq(password_hash),
|
||||||
updated.eq(naive_now()),
|
updated.eq(naive_now()),
|
||||||
|
validator_time.eq(naive_now()),
|
||||||
))
|
))
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
@ -446,6 +449,7 @@ mod tests {
|
||||||
deleted: false,
|
deleted: false,
|
||||||
inbox_url: inserted_user.inbox_url.to_owned(),
|
inbox_url: inserted_user.inbox_url.to_owned(),
|
||||||
shared_inbox_url: None,
|
shared_inbox_url: None,
|
||||||
|
validator_time: inserted_user.published,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
let read_user = User_::read(&conn, inserted_user.id).unwrap();
|
||||||
|
|
|
@ -408,6 +408,7 @@ table! {
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
inbox_url -> Text,
|
inbox_url -> Text,
|
||||||
shared_inbox_url -> Nullable<Text>,
|
shared_inbox_url -> Nullable<Text>,
|
||||||
|
validator_time -> Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ pub struct User_ {
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub inbox_url: DbUrl,
|
pub inbox_url: DbUrl,
|
||||||
pub shared_inbox_url: Option<DbUrl>,
|
pub shared_inbox_url: Option<DbUrl>,
|
||||||
|
pub validator_time: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A safe representation of user, without the sensitive info
|
/// A safe representation of user, without the sensitive info
|
||||||
|
@ -86,6 +87,7 @@ pub struct UserSafeSettings {
|
||||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
pub banner: Option<DbUrl>,
|
pub banner: Option<DbUrl>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
|
pub validator_time: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
|
#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
|
||||||
|
|
|
@ -6,8 +6,14 @@ type Jwt = String;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
|
/// User id, for backward compatibility with client apps.
|
||||||
|
/// Claim [sub](Claims::sub) is used in server-side checks.
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
/// User id, standard claim by RFC 7519.
|
||||||
|
pub sub: i32,
|
||||||
pub iss: String,
|
pub iss: String,
|
||||||
|
/// Time when this token was issued as UNIX-timestamp in seconds
|
||||||
|
pub iat: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
|
@ -26,7 +32,9 @@ impl Claims {
|
||||||
pub fn jwt(user_id: i32, hostname: String) -> Result<Jwt, jsonwebtoken::errors::Error> {
|
pub fn jwt(user_id: i32, hostname: String) -> Result<Jwt, jsonwebtoken::errors::Error> {
|
||||||
let my_claims = Claims {
|
let my_claims = Claims {
|
||||||
id: user_id,
|
id: user_id,
|
||||||
|
sub: user_id,
|
||||||
iss: hostname,
|
iss: hostname,
|
||||||
|
iat: chrono::Utc::now().timestamp_millis() / 1000,
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&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