diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index d15648f7..e4eba00c 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -86,13 +86,15 @@ pub struct RegisterData { name: Option, - #[serde(alias = "orgInviteToken")] token: Option, #[allow(dead_code)] organization_user_id: Option, // Used only from the register/finish endpoint email_verification_token: Option, + accept_emergency_access_id: Option, + accept_emergency_access_invite_token: Option, + org_invite_token: Option, } #[derive(Debug, Deserialize)] @@ -142,23 +144,62 @@ pub async fn _register(data: Json, email_verification: bool, mut c let mut data: RegisterData = data.into_inner(); let email = data.email.to_lowercase(); - if email_verification && data.email_verification_token.is_none() { - err!("Email verification token is required"); - } + let mut email_verified = false; - let email_verified = match &data.email_verification_token { - Some(token) if email_verification => { - let claims = crate::auth::decode_register_verify(token)?; - if claims.sub != data.email { - err!("Email verification token does not match email"); + let mut pending_emergency_access = None; + + // First, validate the provided verification tokens + if email_verification { + match ( + &data.email_verification_token, + &data.accept_emergency_access_id, + &data.accept_emergency_access_invite_token, + &data.organization_user_id, + &data.org_invite_token, + ) { + // Normal user registration, when email verification is required + (Some(email_verification_token), None, None, None, None) => { + let claims = crate::auth::decode_register_verify(email_verification_token)?; + if claims.sub != data.email { + err!("Email verification token does not match email"); + } + + // During this call we don't get the name, so extract it from the claims + if claims.name.is_some() { + data.name = claims.name; + } + email_verified = claims.verified; + } + // Emergency access registration + (None, Some(accept_emergency_access_id), Some(accept_emergency_access_invite_token), None, None) => { + if !CONFIG.emergency_access_allowed() { + err!("Emergency access is not enabled.") + } + + let claims = crate::auth::decode_emergency_access_invite(accept_emergency_access_invite_token)?; + + // This can happen if the user who received the invite used a different email to signup. + // Since we do not know if this is intended, we error out here and do nothing with the invite. + if claims.email != data.email { + err!("Claim email does not match email") + } + if &claims.emer_id != accept_emergency_access_id { + err!("Claim emer_id does not match accept_emergency_access_id") + } + + pending_emergency_access = Some((accept_emergency_access_id, claims)); + email_verified = true; + } + // Org invite + (None, None, None, Some(_organization_user_id), Some(_org_invite_token)) => { + err!("Org invite") } - // During this call, we don't get the name, so extract it from the claims - data.name = Some(claims.name); - claims.verified + _ => { + err!("Registration is missing required parameters") + } } - _ => false, - }; + } // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) // This also prevents issues with very long usernames causing to large JWT's. See #2419 @@ -210,7 +251,10 @@ pub async fn _register(data: Json, email_verification: bool, mut c // Order is important here; the invitation check must come first // because the vaultwarden admin can invite anyone, regardless // of other signup restrictions. - if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) { + if Invitation::take(&email, &mut conn).await + || CONFIG.is_signup_allowed(&email) + || pending_emergency_access.is_some() + { User::new(email.clone()) } else { err!("Registration not allowed or user already exists") @@ -266,10 +310,38 @@ pub async fn _register(data: Json, email_verification: bool, mut c user.save(&mut conn).await?; - // accept any open emergency access invitations - if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() { - for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await { - emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await.ok(); + if CONFIG.emergency_access_allowed() { + // Accept the emergency access invitation + if let Some((accept_emergency_access_id, claims)) = pending_emergency_access { + let Some(mut emergency_access) = + EmergencyAccess::find_by_uuid_and_grantee_email(accept_emergency_access_id, &data.email, &mut conn) + .await + else { + err!("Emergency access not valid.") + }; + + // get grantor user to send Accepted email + let Some(grantor_user) = User::find_by_uuid(&emergency_access.grantor_uuid, &mut conn).await else { + err!("Grantor user not found.") + }; + + if grantor_user.name == claims.grantor_name && grantor_user.email == claims.grantor_email { + emergency_access.accept_invite(&user.uuid, &user.email, &mut conn).await?; + + if CONFIG.mail_enabled() { + mail::send_emergency_access_invite_accepted(&grantor_user.email, &user.email).await?; + } + } else { + err!("Emergency access invitation error.") + } + } + + // accept any open emergency access invitations + if !CONFIG.mail_enabled() { + for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &mut conn).await + { + emergency_invite.accept_invite(&user.uuid, &user.email, &mut conn).await.ok(); + } } } diff --git a/src/api/identity.rs b/src/api/identity.rs index 03923456..86cdd471 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -721,7 +721,7 @@ async fn identity_register(data: Json, conn: DbConn) -> JsonResult #[serde(rename_all = "camelCase")] struct RegisterVerificationData { email: String, - name: String, + name: Option, // receiveMarketingEmails: bool, } @@ -742,22 +742,28 @@ async fn register_verification_email( err!("Registration not allowed or user already exists") } - // TODO: We might want to do some rate limiting here - // Also, test this with invites/emergency access etc + let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify(); if User::find_by_mail(&data.email, &mut conn).await.is_some() { - // TODO: Add some random delay here to prevent timing attacks? + if should_send_mail { + // There is still a timing side channel here in that the code + // paths that send mail take noticeably longer than ones that + // don't. Add a randomized sleep to mitigate this somewhat. + use rand::{rngs::SmallRng, Rng, SeedableRng}; + let mut rng = SmallRng::from_os_rng(); + let delta: i32 = 100; + let sleep_ms = (1_000 + rng.random_range(-delta..=delta)) as u64; + tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await; + } return Ok(RegisterVerificationResponse::NoContent(())); } - let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify(); - let token_claims = crate::auth::generate_register_verify_claims(data.email.clone(), data.name.clone(), should_send_mail); let token = crate::auth::encode_jwt(&token_claims); if should_send_mail { - mail::send_register_verify_email(&data.email, &data.name, &token).await?; + mail::send_register_verify_email(&data.email, &token).await?; Ok(RegisterVerificationResponse::NoContent(())) } else { diff --git a/src/auth.rs b/src/auth.rs index d446109a..0fabd6a4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -331,11 +331,11 @@ pub struct RegisterVerifyClaims { // Subject pub sub: String, - pub name: String, + pub name: Option, pub verified: bool, } -pub fn generate_register_verify_claims(email: String, name: String, verified: bool) -> RegisterVerifyClaims { +pub fn generate_register_verify_claims(email: String, name: Option, verified: bool) -> RegisterVerifyClaims { let time_now = Utc::now(); RegisterVerifyClaims { nbf: time_now.timestamp(), diff --git a/src/mail.rs b/src/mail.rs index 7a4deaec..015d8acb 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -201,7 +201,7 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } -pub async fn send_register_verify_email(email: &str, name: &str, token: &str) -> EmptyResult { +pub async fn send_register_verify_email(email: &str, token: &str) -> EmptyResult { let mut query = url::Url::parse("https://query.builder").unwrap(); query.query_pairs_mut().append_pair("email", email).append_pair("token", token); let query_string = match query.query() { @@ -215,7 +215,6 @@ pub async fn send_register_verify_email(email: &str, name: &str, token: &str) -> // `url.Url` would place the anchor `#` after the query parameters "url": format!("{}/#/finish-signup/?{}", CONFIG.domain(), query_string), "img_src": CONFIG._smtp_img_src(), - "name": name, "email": email, }), )?; diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 42c4d8dc..cdc1e266 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -93,12 +93,19 @@ bit-nav-logo bit-nav-item .bwi-shield { /**** END Static Vaultwarden Changes ****/ /**** START Dynamic Vaultwarden Changes ****/ {{#if signup_disabled}} +/* From web vault 2025.1.2 and onwards, the signup button is hidden + when signups are disabled as the web vault checks the /api/config endpoint. + Note that the clients tend to aggressively cache this endpoint, so it might + take a while for the change to take effect. To avoid the button appearing + when it shouldn't, we'll keep this style in place for a couple of versions */ +{{#if webver "<2025.3.0"}} /* Hide the register link on the login screen */ app-login form div + div + div + div + hr, app-login form div + div + div + div + hr + p { @extend %vw-hide; } {{/if}} +{{/if}} {{#unless mail_enabled}} /* Hide `Email` 2FA if mail is not enabled */