Merge pull request #1500 from LemmyNet/jwt_revocation_dess

Jwt revocation dess
This commit is contained in:
Nutomic 2021-03-19 15:43:47 +00:00 committed by GitHub
commit 14bc9f0946
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 31 deletions

View file

@ -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() {

View file

@ -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)?,
}) })
} }
} }

View file

@ -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)
} }

View file

@ -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,
} }
} }

View file

@ -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,
} }

View file

@ -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;

View file

@ -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(),

View file

@ -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()
}
}); });
} }

View file

@ -0,0 +1 @@
alter table local_user drop column validator_time;

View file

@ -0,0 +1 @@
alter table local_user add column validator_time timestamp not null default now();