From 475c7b8f1671ba74001bbe50050c1a69931122cb Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sun, 9 Oct 2022 13:28:41 +0200 Subject: [PATCH 1/7] return more descriptive JWT validation messages --- src/auth.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index f99fbd39..c0d2f3e2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,18 +1,14 @@ -// // JWT Handling // use chrono::{Duration, Utc}; use num_traits::FromPrimitive; use once_cell::sync::Lazy; -use jsonwebtoken::{self, Algorithm, DecodingKey, EncodingKey, Header}; +use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; use serde::de::DeserializeOwned; use serde::ser::Serialize; -use crate::{ - error::{Error, MapResult}, - CONFIG, -}; +use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -61,7 +57,15 @@ fn decode_jwt(token: &str, issuer: String) -> Result Ok(d.claims), + Err(err) => match *err.kind() { + ErrorKind::InvalidToken => err!("Token is invalid"), + ErrorKind::InvalidIssuer => err!("Issuer is invalid"), + ErrorKind::ExpiredSignature => err!("Token has expired"), + _ => err!("Error decoding JWT"), + }, + } } pub fn decode_login(token: &str) -> Result { From 2d7ffbf378350872bc38970b4cf9105ee99038b1 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Tue, 27 Sep 2022 10:10:09 +0200 Subject: [PATCH 2/7] allow the removal of non-confirmed owners ensure user_to_edit and user_to_delete are actually confirmed users, before checking if they are the last owner of an organization. --- src/api/core/organizations.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 3934de88..dca4f393 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -999,8 +999,11 @@ async fn edit_user( err!("Only Owners can edit Owner users") } - if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { - // Removing owner permmission, check that there is at least one other confirmed owner + if user_to_edit.atype == UserOrgType::Owner + && new_type != UserOrgType::Owner + && user_to_edit.status == UserOrgStatus::Confirmed as i32 + { + // Removing owner permission, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &conn).await <= 1 { err!("Can't delete the last owner") } @@ -1097,7 +1100,7 @@ async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, c err!("Only Owners can delete Admins or Owners") } - if user_to_delete.atype == UserOrgType::Owner { + if user_to_delete.atype == UserOrgType::Owner && user_to_delete.status == UserOrgStatus::Confirmed as i32 { // Removing owner, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 { err!("Can't delete the last owner") From 1704d14f29c485f90cd6b9c4f06109dd259c6685 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Thu, 6 Oct 2022 21:26:49 +0200 Subject: [PATCH 3/7] v2022.9.2 expects a json response when registering --- src/api/core/accounts.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 35202698..4508c5fd 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -81,7 +81,7 @@ fn enforce_password_hint_setting(password_hint: &Option) -> EmptyResult } #[post("/accounts/register", data = "")] -async fn register(data: JsonUpcase, conn: DbConn) -> EmptyResult { +async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { let data: RegisterData = data.into_inner().data; let email = data.Email.to_lowercase(); @@ -178,7 +178,10 @@ async fn register(data: JsonUpcase, conn: DbConn) -> EmptyResult { } } - user.save(&conn).await + user.save(&conn).await?; + Ok(Json(json!({ + "registration": "success", + }))) } #[get("/accounts/profile")] From 2c0742387bd65cd450361a8614903247212556c3 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Fri, 7 Oct 2022 06:33:29 +0200 Subject: [PATCH 4/7] return CaptchaBypassToken and register object --- src/api/core/accounts.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 4508c5fd..429768fc 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -180,7 +180,8 @@ async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { user.save(&conn).await?; Ok(Json(json!({ - "registration": "success", + "object": "register", + "CaptchaBypassToken": "", }))) } From 5b96270874d273a2464e3118229665cf6f1724ee Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sat, 8 Oct 2022 10:27:33 +0200 Subject: [PATCH 5/7] return "Object" for consistency Co-authored-by: Jeremy Lin --- src/api/core/accounts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 429768fc..a980271b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -180,7 +180,7 @@ async fn register(data: JsonUpcase, conn: DbConn) -> JsonResult { user.save(&conn).await?; Ok(Json(json!({ - "object": "register", + "Object": "register", "CaptchaBypassToken": "", }))) } From c78d383ed12d00256d93e1e11c0a81f6ee2fa208 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 8 Oct 2022 18:31:34 +0200 Subject: [PATCH 6/7] make invitation expiration time configurable configure the number of hours after which organization invites, emergency access invites, email verification emails and account deletion requests expire (defaults to 5 days or 120 hours and must be atleast 1) --- .env.template | 4 ++++ src/auth.rs | 12 ++++++++---- src/config.rs | 7 +++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.env.template b/.env.template index 66a04343..60b5b73b 100644 --- a/.env.template +++ b/.env.template @@ -245,6 +245,10 @@ ## Name shown in the invitation emails that don't come from a specific organization # INVITATION_ORG_NAME=Vaultwarden +## The number of hours after which an organization invite token, emergency access invite token, +## email verification token and deletion request token will expire (must be at least 1) +# INVITATION_EXPIRATION_HOURS=120 + ## Per-organization attachment storage limit (KB) ## Max kilobytes of attachment storage allowed per organization. ## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization. diff --git a/src/auth.rs b/src/auth.rs index c0d2f3e2..252766db 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -152,9 +152,10 @@ pub fn generate_invite_claims( invited_by_email: Option, ) -> InviteJwtClaims { let time_now = Utc::now().naive_utc(); + let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); InviteJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + Duration::days(5)).timestamp(), + exp: (time_now + Duration::hours(expire_hours)).timestamp(), iss: JWT_INVITE_ISSUER.to_string(), sub: uuid, email, @@ -189,9 +190,10 @@ pub fn generate_emergency_access_invite_claims( grantor_email: Option, ) -> EmergencyAccessInviteJwtClaims { let time_now = Utc::now().naive_utc(); + let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); EmergencyAccessInviteJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + Duration::days(5)).timestamp(), + exp: (time_now + Duration::hours(expire_hours)).timestamp(), iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(), sub: uuid, email, @@ -215,9 +217,10 @@ pub struct BasicJwtClaims { pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims { let time_now = Utc::now().naive_utc(); + let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); BasicJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + Duration::days(5)).timestamp(), + exp: (time_now + Duration::hours(expire_hours)).timestamp(), iss: JWT_DELETE_ISSUER.to_string(), sub: uuid, } @@ -225,9 +228,10 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims { pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims { let time_now = Utc::now().naive_utc(); + let expire_hours = i64::from(CONFIG.invitation_expiration_hours()); BasicJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + Duration::days(5)).timestamp(), + exp: (time_now + Duration::hours(expire_hours)).timestamp(), iss: JWT_VERIFYEMAIL_ISSUER.to_string(), sub: uuid, } diff --git a/src/config.rs b/src/config.rs index b8f3246b..1d9e53f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -430,6 +430,9 @@ make_config! { org_creation_users: String, true, def, "".to_string(); /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are otherwise disabled invitations_allowed: bool, true, def, true; + /// Invitation token expiration time (in hours) |> The number of hours after which an organization invite token, emergency access invite token, + /// email verification token and deletion request token will expire (must be at least 1) + invitation_expiration_hours: u32, false, def, 120; /// Allow emergency access |> Controls whether users can enable emergency access to their accounts. This setting applies globally to all users. emergency_access_allowed: bool, true, def, true; /// Password iterations |> Number of server-side passwords hashing iterations. @@ -726,6 +729,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { _ => err!("Only HTTP 301/302 and 307/308 redirects are supported"), } + if cfg.invitation_expiration_hours < 1 { + err!("`INVITATION_EXPIRATION_HOURS` has a minimum size of 1") + } + Ok(()) } From ef4072e4ffb47a1c24fc6c0ac8144231fb1d5cfd Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Sun, 9 Oct 2022 05:50:43 +0200 Subject: [PATCH 7/7] improve spelling of minimum expiration hours check Co-authored-by: Helmut K. C. Tessarek --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 1d9e53f5..3a2cf958 100644 --- a/src/config.rs +++ b/src/config.rs @@ -730,7 +730,7 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } if cfg.invitation_expiration_hours < 1 { - err!("`INVITATION_EXPIRATION_HOURS` has a minimum size of 1") + err!("`INVITATION_EXPIRATION_HOURS` has a minimum duration of 1 hour") } Ok(())