mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-29 00:42:19 +00:00
Merge pull request #1500 from LemmyNet/jwt_revocation_dess
Jwt revocation dess
This commit is contained in:
commit
14bc9f0946
|
@ -41,7 +41,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;
|
||||||
|
@ -100,16 +100,32 @@ pub(crate) async fn get_local_user_view_from_jwt(
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
|
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 =
|
let local_user_view =
|
||||||
blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
|
blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
|
||||||
// Check for a site ban
|
// Check for a site ban
|
||||||
if local_user_view.person.banned {
|
if local_user_view.person.banned {
|
||||||
return Err(ApiError::err("site_ban").into());
|
return Err(ApiError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
|
||||||
|
|
||||||
Ok(local_user_view)
|
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();
|
||||||
|
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(
|
pub(crate) async fn get_local_user_view_from_jwt_opt(
|
||||||
jwt: &Option<String>,
|
jwt: &Option<String>,
|
||||||
pool: &DbPool,
|
pool: &DbPool,
|
||||||
|
@ -128,7 +144,7 @@ pub(crate) async fn get_local_user_settings_view_from_jwt(
|
||||||
Ok(claims) => claims.claims,
|
Ok(claims) => claims.claims,
|
||||||
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
|
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| {
|
let local_user_view = blocking(pool, move |conn| {
|
||||||
LocalUserSettingsView::read(conn, local_user_id)
|
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 {
|
if local_user_view.person.banned {
|
||||||
return Err(ApiError::err("site_ban").into());
|
return Err(ApiError::err("site_ban").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
|
||||||
|
|
||||||
Ok(local_user_view)
|
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> {
|
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")
|
||||||
|
@ -491,7 +514,70 @@ 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, 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]
|
#[test]
|
||||||
fn test_espeak() {
|
fn test_espeak() {
|
||||||
|
|
|
@ -130,7 +130,7 @@ impl Perform for Login {
|
||||||
|
|
||||||
// Return the jwt
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
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
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
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
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
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
|
// Return the jwt
|
||||||
Ok(LoginResponse {
|
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 bcrypt::{hash, DEFAULT_COST};
|
||||||
use diesel::{dsl::*, result::Error, *};
|
use diesel::{dsl::*, result::Error, *};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
naive_now,
|
||||||
schema::local_user::dsl::*,
|
schema::local_user::dsl::*,
|
||||||
source::local_user::{LocalUser, LocalUserForm},
|
source::local_user::{LocalUser, LocalUserForm},
|
||||||
LocalUserId,
|
LocalUserId,
|
||||||
PersonId,
|
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 {
|
mod safe_settings_type {
|
||||||
use crate::ToSafeSettings;
|
use crate::ToSafeSettings;
|
||||||
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
|
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
|
||||||
|
@ -39,6 +26,7 @@ mod safe_settings_type {
|
||||||
show_avatars,
|
show_avatars,
|
||||||
send_notifications_to_email,
|
send_notifications_to_email,
|
||||||
matrix_user_id,
|
matrix_user_id,
|
||||||
|
validator_time,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl ToSafeSettings for LocalUser {
|
impl ToSafeSettings for LocalUser {
|
||||||
|
@ -59,6 +47,7 @@ mod safe_settings_type {
|
||||||
show_avatars,
|
show_avatars,
|
||||||
send_notifications_to_email,
|
send_notifications_to_email,
|
||||||
matrix_user_id,
|
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");
|
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
|
||||||
|
|
||||||
diesel::update(local_user.find(local_user_id))
|
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)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,6 +155,7 @@ table! {
|
||||||
show_avatars -> Bool,
|
show_avatars -> Bool,
|
||||||
send_notifications_to_email -> Bool,
|
send_notifications_to_email -> Bool,
|
||||||
matrix_user_id -> Nullable<Text>,
|
matrix_user_id -> Nullable<Text>,
|
||||||
|
validator_time -> Timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub struct LocalUser {
|
||||||
pub show_avatars: bool,
|
pub show_avatars: bool,
|
||||||
pub send_notifications_to_email: bool,
|
pub send_notifications_to_email: bool,
|
||||||
pub matrix_user_id: Option<String>,
|
pub matrix_user_id: Option<String>,
|
||||||
|
pub validator_time: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO redo these, check table defaults
|
// TODO redo these, check table defaults
|
||||||
|
@ -53,4 +54,5 @@ pub struct LocalUserSettings {
|
||||||
pub show_avatars: bool,
|
pub show_avatars: bool,
|
||||||
pub send_notifications_to_email: bool,
|
pub send_notifications_to_email: bool,
|
||||||
pub matrix_user_id: Option<String>,
|
pub matrix_user_id: Option<String>,
|
||||||
|
pub validator_time: chrono::NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,7 +227,7 @@ fn get_feed_front(
|
||||||
jwt: String,
|
jwt: String,
|
||||||
) -> Result<ChannelBuilder, LemmyError> {
|
) -> Result<ChannelBuilder, LemmyError> {
|
||||||
let site_view = SiteView::read(&conn)?;
|
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 person_id = LocalUser::read(&conn, local_user_id)?.person_id;
|
||||||
|
|
||||||
let posts = PostQueryBuilder::create(&conn)
|
let posts = PostQueryBuilder::create(&conn)
|
||||||
|
@ -254,7 +254,7 @@ fn get_feed_front(
|
||||||
|
|
||||||
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
|
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
|
||||||
let site_view = SiteView::read(&conn)?;
|
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 person_id = LocalUser::read(&conn, local_user_id)?.person_id;
|
||||||
|
|
||||||
let sort = SortType::New;
|
let sort = SortType::New;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::settings::structs::Settings;
|
use crate::settings::structs::Settings;
|
||||||
|
use chrono::Utc;
|
||||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -6,8 +7,11 @@ type Jwt = String;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub local_user_id: i32,
|
/// local_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 {
|
||||||
|
@ -23,10 +27,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 {
|
let my_claims = Claims {
|
||||||
local_user_id,
|
sub: local_user_id,
|
||||||
iss: hostname,
|
iss: Settings::get().hostname(),
|
||||||
|
iat: Utc::now().timestamp(),
|
||||||
};
|
};
|
||||||
encode(
|
encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
|
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use deser_hjson::from_str;
|
use deser_hjson::from_str;
|
||||||
|
use log::warn;
|
||||||
use merge::Merge;
|
use merge::Merge;
|
||||||
use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
|
use std::{env, fs, io::Error, net::IpAddr, sync::RwLock};
|
||||||
|
|
||||||
|
@ -24,7 +25,13 @@ static CONFIG_FILE: &str = "config/config.hjson";
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
|
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => panic!("{}", e),
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Couldn't load settings file, using default settings.\n{}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
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