From cf214ff5833013b7dfc84f7101fa1cf6df6cdf9e Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 20 Sep 2021 17:46:34 +0200 Subject: [PATCH] Move jwt secret from config to database (fixes #1728) --- ansible/templates/config.hjson | 2 - config/config.hjson | 2 - crates/api/src/lib.rs | 12 ++++-- crates/api/src/local_user.rs | 14 +++++-- crates/api_common/src/lib.rs | 8 +++- crates/api_crud/src/user/create.rs | 6 ++- crates/db_queries/src/source/mod.rs | 1 + crates/db_queries/src/source/secret.rs | 37 +++++++++++++++++++ crates/db_schema/src/schema.rs | 7 ++++ crates/db_schema/src/source/mod.rs | 1 + crates/db_schema/src/source/secret.rs | 8 ++++ crates/routes/src/feeds.rs | 19 +++++----- crates/routes/src/images.rs | 5 ++- crates/utils/Cargo.toml | 2 +- crates/utils/src/claims.rs | 21 ++++------- crates/utils/src/lib.rs | 2 +- crates/utils/src/settings/structs.rs | 2 - docker/federation/lemmy_alpha.hjson | 1 - docker/federation/lemmy_beta.hjson | 1 - docker/federation/lemmy_delta.hjson | 1 - docker/federation/lemmy_epsilon.hjson | 1 - docker/federation/lemmy_gamma.hjson | 1 - docker/lemmy.hjson | 2 - .../2021-09-20-112945_jwt-secret/down.sql | 1 + .../2021-09-20-112945_jwt-secret/up.sql | 9 +++++ 25 files changed, 116 insertions(+), 50 deletions(-) create mode 100644 crates/db_queries/src/source/secret.rs create mode 100644 crates/db_schema/src/source/secret.rs create mode 100644 migrations/2021-09-20-112945_jwt-secret/down.sql create mode 100644 migrations/2021-09-20-112945_jwt-secret/up.sql diff --git a/ansible/templates/config.hjson b/ansible/templates/config.hjson index de98e4a83..fd7c3176e 100644 --- a/ansible/templates/config.hjson +++ b/ansible/templates/config.hjson @@ -16,8 +16,6 @@ hostname: "{{ domain }}" # the port where lemmy should listen for incoming requests port: 8536 - # json web token for authorization between server and client - jwt_secret: "{{ jwt_password }}" # whether tls is required for activitypub. only disable this for debugging, never for producion. tls_enabled: true # address where pictrs is available diff --git a/config/config.hjson b/config/config.hjson index cb08dda45..11b64b204 100644 --- a/config/config.hjson +++ b/config/config.hjson @@ -33,8 +33,6 @@ port: 8536 # whether tls is required for activitypub. only disable this for debugging, never for producion. tls_enabled: true - # json web token for authorization between server and client - jwt_secret: "changeme" # address where pictrs is available pictrs_url: "http://pictrs:8080" # maximum length of local community and user names diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 83d98bde5..1146975f0 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -188,10 +188,15 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> String { #[cfg(test)] mod tests { use lemmy_api_common::check_validator_time; - use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud}; + use lemmy_db_queries::{ + establish_unpooled_connection, + source::{local_user::LocalUser_, secret::SecretSingleton}, + Crud, + }; use lemmy_db_schema::source::{ local_user::{LocalUser, LocalUserForm}, person::{Person, PersonForm}, + secret::Secret, }; use lemmy_utils::claims::Claims; @@ -214,8 +219,9 @@ mod tests { 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 jwt_secret = Secret::get().jwt_secret; + let jwt = Claims::jwt(inserted_local_user.id.0, &jwt_secret).unwrap(); + let claims = Claims::decode(&jwt, jwt_secret.as_ref()).unwrap().claims; let check = check_validator_time(&inserted_local_user.validator_time, &claims); assert!(check.is_ok()); diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs index 510ef5adb..df03e3020 100644 --- a/crates/api/src/local_user.rs +++ b/crates/api/src/local_user.rs @@ -25,6 +25,7 @@ use lemmy_db_queries::{ person_mention::PersonMention_, post::Post_, private_message::PrivateMessage_, + secret::SecretSingleton, }, Blockable, Crud, @@ -43,6 +44,7 @@ use lemmy_db_schema::{ person_mention::*, post::Post, private_message::PrivateMessage, + secret::Secret, site::*, }, }; @@ -103,8 +105,9 @@ impl Perform for Login { } // Return the jwt + let jwt_secret = Secret::get().jwt_secret; Ok(LoginResponse { - jwt: Claims::jwt(local_user_view.local_user.id.0)?, + jwt: Claims::jwt(local_user_view.local_user.id.0, &jwt_secret)?, }) } } @@ -268,8 +271,9 @@ impl Perform for SaveUserSettings { }; // Return the jwt + let jwt_secret = Secret::get().jwt_secret; Ok(LoginResponse { - jwt: Claims::jwt(updated_local_user.id.0)?, + jwt: Claims::jwt(updated_local_user.id.0, &jwt_secret)?, }) } } @@ -311,8 +315,9 @@ impl Perform for ChangePassword { .await??; // Return the jwt + let jwt_secret = Secret::get().jwt_secret; Ok(LoginResponse { - jwt: Claims::jwt(updated_local_user.id.0)?, + jwt: Claims::jwt(updated_local_user.id.0, &jwt_secret)?, }) } } @@ -770,8 +775,9 @@ impl Perform for PasswordChange { .map_err(|_| ApiError::err("couldnt_update_user"))?; // Return the jwt + let jwt_secret = Secret::get().jwt_secret; Ok(LoginResponse { - jwt: Claims::jwt(updated_local_user.id.0)?, + jwt: Claims::jwt(updated_local_user.id.0, &jwt_secret)?, }) } } diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 933a096c5..068bd253f 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -11,6 +11,7 @@ use lemmy_db_queries::{ source::{ community::{CommunityModerator_, Community_}, person_block::PersonBlock_, + secret::SecretSingleton, site::Site_, }, Crud, @@ -25,6 +26,7 @@ use lemmy_db_schema::{ person_block::PersonBlock, person_mention::{PersonMention, PersonMentionForm}, post::{Post, PostRead, PostReadForm}, + secret::Secret, site::Site, }, CommunityId, @@ -245,7 +247,8 @@ pub async fn get_local_user_view_from_jwt( jwt: &str, pool: &DbPool, ) -> Result { - let claims = Claims::decode(jwt) + let jwt_secret = Secret::get().jwt_secret; + let claims = Claims::decode(jwt, &jwt_secret) .map_err(|_| ApiError::err("not_logged_in"))? .claims; let local_user_id = LocalUserId(claims.sub); @@ -293,7 +296,8 @@ pub async fn get_local_user_settings_view_from_jwt( jwt: &str, pool: &DbPool, ) -> Result { - let claims = Claims::decode(jwt) + let jwt_secret = Secret::get().jwt_secret; + let claims = Claims::decode(jwt, &jwt_secret) .map_err(|_| ApiError::err("not_logged_in"))? .claims; let local_user_id = LocalUserId(claims.sub); diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 6f9216b36..24b734623 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -9,7 +9,7 @@ use lemmy_apub::{ EndpointType, }; use lemmy_db_queries::{ - source::{local_user::LocalUser_, site::Site_}, + source::{local_user::LocalUser_, secret::SecretSingleton, site::Site_}, Crud, Followable, Joinable, @@ -21,6 +21,7 @@ use lemmy_db_schema::{ community::*, local_user::{LocalUser, LocalUserForm}, person::*, + secret::Secret, site::*, }, CommunityId, @@ -218,8 +219,9 @@ impl PerformCrud for Register { } // Return the jwt + let jwt_secret = Secret::get().jwt_secret; Ok(LoginResponse { - jwt: Claims::jwt(inserted_local_user.id.0)?, + jwt: Claims::jwt(inserted_local_user.id.0, &jwt_secret)?, }) } } diff --git a/crates/db_queries/src/source/mod.rs b/crates/db_queries/src/source/mod.rs index 8b82d31a7..a1e45efa5 100644 --- a/crates/db_queries/src/source/mod.rs +++ b/crates/db_queries/src/source/mod.rs @@ -12,4 +12,5 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod secret; pub mod site; diff --git a/crates/db_queries/src/source/secret.rs b/crates/db_queries/src/source/secret.rs new file mode 100644 index 000000000..eac0d7eb4 --- /dev/null +++ b/crates/db_queries/src/source/secret.rs @@ -0,0 +1,37 @@ +use diesel::{result::Error, *}; +use lemmy_db_schema::source::secret::Secret; +use lemmy_utils::settings::structs::Settings; +use std::sync::RwLock; + +use crate::get_database_url_from_env; + +lazy_static! { + static ref SECRET: RwLock = RwLock::new(init().expect("Failed to load secrets from DB.")); +} + +pub trait SecretSingleton { + fn get() -> Secret; +} + +impl SecretSingleton for Secret { + /// Returns the Secret as a struct + fn get() -> Self { + SECRET.read().expect("read secrets").to_owned() + } +} + +/// Reads the secrets from the DB +fn init() -> Result { + let db_url = match get_database_url_from_env() { + Ok(url) => url, + Err(_) => Settings::get().get_database_url(), + }; + + let conn = PgConnection::establish(&db_url).expect("Couldn't get DB connection for Secrets."); + read_secrets(&conn) +} + +fn read_secrets(conn: &PgConnection) -> Result { + use lemmy_db_schema::schema::secret::dsl::*; + secret.first::(conn) +} diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 4c92fffba..b08028900 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -551,6 +551,13 @@ table! { } } +table! { + secret(id) { + id -> Int4, + jwt_secret -> Varchar, + } +} + joinable!(comment_alias_1 -> person_alias_1 (creator_id)); joinable!(comment -> comment_alias_1 (parent_id)); joinable!(person_mention -> person_alias_1 (recipient_id)); diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 8b82d31a7..a1e45efa5 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -12,4 +12,5 @@ pub mod person_mention; pub mod post; pub mod post_report; pub mod private_message; +pub mod secret; pub mod site; diff --git a/crates/db_schema/src/source/secret.rs b/crates/db_schema/src/source/secret.rs new file mode 100644 index 000000000..1a8b30185 --- /dev/null +++ b/crates/db_schema/src/source/secret.rs @@ -0,0 +1,8 @@ +use crate::schema::secret; + +#[derive(Queryable, Identifiable, Clone)] +#[table_name = "secret"] +pub struct Secret { + pub id: i32, + pub jwt_secret: String, +} diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 75542e740..5cf2f1759 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -4,13 +4,13 @@ use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::PgConnection; use lemmy_api_common::blocking; use lemmy_db_queries::{ - source::{community::Community_, person::Person_}, + source::{community::Community_, person::Person_, secret::SecretSingleton}, Crud, ListingType, SortType, }; use lemmy_db_schema::{ - source::{community::Community, local_user::LocalUser, person::Person}, + source::{community::Community, local_user::LocalUser, person::Person, secret::Secret}, LocalUserId, }; use lemmy_db_views::{ @@ -229,17 +229,15 @@ fn get_feed_front( jwt: String, ) -> Result { let site_view = SiteView::read(conn)?; - let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); + let jwt_secret = Secret::get().jwt_secret; + let local_user_id = LocalUserId(Claims::decode(&jwt, &jwt_secret)?.claims.sub); let local_user = LocalUser::read(conn, local_user_id)?; - let person_id = local_user.person_id; - let show_bot_accounts = local_user.show_bot_accounts; - let show_read_posts = local_user.show_read_posts; let posts = PostQueryBuilder::create(conn) .listing_type(ListingType::Subscribed) - .my_person_id(person_id) - .show_bot_accounts(show_bot_accounts) - .show_read_posts(show_read_posts) + .my_person_id(local_user.person_id) + .show_bot_accounts(local_user.show_bot_accounts) + .show_read_posts(local_user.show_read_posts) .sort(*sort_type) .list()?; @@ -261,7 +259,8 @@ fn get_feed_front( fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result { let site_view = SiteView::read(conn)?; - let local_user_id = LocalUserId(Claims::decode(&jwt)?.claims.sub); + let jwt_secret = Secret::get().jwt_secret; + let local_user_id = LocalUserId(Claims::decode(&jwt, &jwt_secret)?.claims.sub); let local_user = LocalUser::read(conn, local_user_id)?; let person_id = local_user.person_id; let show_bot_accounts = local_user.show_bot_accounts; diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index 7439e4a8e..230841d44 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -2,6 +2,8 @@ use actix_http::http::header::ACCEPT_ENCODING; use actix_web::{body::BodyStream, http::StatusCode, web::Data, *}; use anyhow::anyhow; use awc::Client; +use lemmy_db_queries::source::secret::SecretSingleton; +use lemmy_db_schema::source::secret::Secret; use lemmy_utils::{claims::Claims, rate_limit::RateLimit, settings::structs::Settings, LemmyError}; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -52,7 +54,8 @@ async fn upload( .cookie("jwt") .expect("No auth header for picture upload"); - if Claims::decode(jwt.value()).is_err() { + let jwt_secret = Secret::get().jwt_secret; + if Claims::decode(jwt.value(), &jwt_secret).is_err() { return Ok(HttpResponse::Unauthorized().finish()); }; diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index fda58cd16..477833583 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -35,7 +35,7 @@ strum_macros = "0.21.1" futures = "0.3.16" diesel = "1.4.7" http = "0.2.4" -jsonwebtoken = "7.2.0" deser-hjson = "1.0.2" smart-default = "0.6.0" webpage = { version = "1.3.0", default-features = false, features = ["serde"] } +jsonwebtoken = "7.2.0" diff --git a/crates/utils/src/claims.rs b/crates/utils/src/claims.rs index e1c3ba158..2b6ce9854 100644 --- a/crates/utils/src/claims.rs +++ b/crates/utils/src/claims.rs @@ -1,4 +1,4 @@ -use crate::settings::structs::Settings; +use crate::{settings::structs::Settings, LemmyError}; use chrono::Utc; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use serde::{Deserialize, Serialize}; @@ -15,28 +15,23 @@ pub struct Claims { } impl Claims { - pub fn decode(jwt: &str) -> Result, jsonwebtoken::errors::Error> { + pub fn decode(jwt: &str, jwt_secret: &str) -> Result, LemmyError> { let v = Validation { validate_exp: false, ..Validation::default() }; - decode::( - jwt, - &DecodingKey::from_secret(Settings::get().jwt_secret.as_ref()), - &v, - ) + let key = DecodingKey::from_secret(jwt_secret.as_ref()); + Ok(decode::(jwt, &key, &v)?) } - pub fn jwt(local_user_id: i32) -> Result { + pub fn jwt(local_user_id: i32, jwt_secret: &str) -> Result { let my_claims = Claims { sub: local_user_id, iss: Settings::get().hostname, iat: Utc::now().timestamp(), }; - encode( - &Header::default(), - &my_claims, - &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()), - ) + + let key = EncodingKey::from_secret(jwt_secret.as_ref()); + Ok(encode(&Header::default(), &my_claims, &key)?) } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 6bf3237e4..f1093fd58 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -6,12 +6,12 @@ extern crate strum_macros; extern crate smart_default; pub mod apub; -pub mod claims; pub mod email; pub mod rate_limit; pub mod request; pub mod settings; +pub mod claims; #[cfg(test)] mod test; pub mod utils; diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 074cf87e8..9ba8a5f94 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -24,8 +24,6 @@ pub struct Settings { pub port: u16, #[default(true)] pub tls_enabled: bool, - #[default("changeme")] - pub jwt_secret: String, #[default(None)] pub pictrs_url: Option, #[default(None)] diff --git a/docker/federation/lemmy_alpha.hjson b/docker/federation/lemmy_alpha.hjson index 3795f0d79..8c69a4c6f 100644 --- a/docker/federation/lemmy_alpha.hjson +++ b/docker/federation/lemmy_alpha.hjson @@ -2,7 +2,6 @@ hostname: lemmy-alpha:8541 port: 8541 tls_enabled: false - jwt_secret: changeme setup: { admin_username: lemmy_alpha admin_password: lemmylemmy diff --git a/docker/federation/lemmy_beta.hjson b/docker/federation/lemmy_beta.hjson index 1c0b3876b..efd9458cc 100644 --- a/docker/federation/lemmy_beta.hjson +++ b/docker/federation/lemmy_beta.hjson @@ -2,7 +2,6 @@ hostname: lemmy-beta:8551 port: 8551 tls_enabled: false - jwt_secret: changeme setup: { admin_username: lemmy_beta admin_password: lemmylemmy diff --git a/docker/federation/lemmy_delta.hjson b/docker/federation/lemmy_delta.hjson index 2f7dfecb4..75f3f9163 100644 --- a/docker/federation/lemmy_delta.hjson +++ b/docker/federation/lemmy_delta.hjson @@ -2,7 +2,6 @@ hostname: lemmy-delta:8571 port: 8571 tls_enabled: false - jwt_secret: changeme setup: { admin_username: lemmy_delta admin_password: lemmylemmy diff --git a/docker/federation/lemmy_epsilon.hjson b/docker/federation/lemmy_epsilon.hjson index 87d142a8f..1b2cbd34c 100644 --- a/docker/federation/lemmy_epsilon.hjson +++ b/docker/federation/lemmy_epsilon.hjson @@ -2,7 +2,6 @@ hostname: lemmy-epsilon:8581 port: 8581 tls_enabled: false - jwt_secret: changeme setup: { admin_username: lemmy_epsilon admin_password: lemmylemmy diff --git a/docker/federation/lemmy_gamma.hjson b/docker/federation/lemmy_gamma.hjson index 2b94a968d..48b6c1c53 100644 --- a/docker/federation/lemmy_gamma.hjson +++ b/docker/federation/lemmy_gamma.hjson @@ -2,7 +2,6 @@ hostname: lemmy-gamma:8561 port: 8561 tls_enabled: false - jwt_secret: changeme setup: { admin_username: lemmy_gamma admin_password: lemmylemmy diff --git a/docker/lemmy.hjson b/docker/lemmy.hjson index de7cf68a0..4c42d8aea 100644 --- a/docker/lemmy.hjson +++ b/docker/lemmy.hjson @@ -17,8 +17,6 @@ bind: "0.0.0.0" # port where lemmy should listen for incoming requests port: 8536 - # json web token for authorization between server and client - jwt_secret: "changeme" # settings related to the postgresql database # address where pictrs is available pictrs_url: "http://pictrs:8080" diff --git a/migrations/2021-09-20-112945_jwt-secret/down.sql b/migrations/2021-09-20-112945_jwt-secret/down.sql new file mode 100644 index 000000000..61b21fbab --- /dev/null +++ b/migrations/2021-09-20-112945_jwt-secret/down.sql @@ -0,0 +1 @@ +drop table secret; diff --git a/migrations/2021-09-20-112945_jwt-secret/up.sql b/migrations/2021-09-20-112945_jwt-secret/up.sql new file mode 100644 index 000000000..e9af9ef55 --- /dev/null +++ b/migrations/2021-09-20-112945_jwt-secret/up.sql @@ -0,0 +1,9 @@ +-- generate a jwt secret +create extension if not exists pgcrypto; + +create table secret( + id serial primary key, + jwt_secret varchar not null default gen_random_uuid() +); + +insert into secret default values;