mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-03-23 14:04:17 -04:00
Implement new registration flow with email verification (#5215)
* Implement registration with required verified email * Optional name, emergency access, and signups_allowed * Implement org invite, remove unneeded invite accept * fix invitation logic for new registration flow (#5691) * fix invitation logic for new registration flow * clarify email_2fa_enforce_on_verified_invite --------- Co-authored-by: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com>
This commit is contained in:
parent
71952a4ab5
commit
2a18665288
@ -229,7 +229,8 @@
|
|||||||
# SIGNUPS_ALLOWED=true
|
# SIGNUPS_ALLOWED=true
|
||||||
|
|
||||||
## Controls if new users need to verify their email address upon registration
|
## Controls if new users need to verify their email address upon registration
|
||||||
## Note that setting this option to true prevents logins until the email address has been verified!
|
## On new client versions, this will require the user to verify their email at signup time.
|
||||||
|
## On older clients, it will require the user to verify their email before they can log in.
|
||||||
## The welcome email will include a verification link, and login attempts will periodically
|
## The welcome email will include a verification link, and login attempts will periodically
|
||||||
## trigger another verification email to be sent.
|
## trigger another verification email to be sent.
|
||||||
# SIGNUPS_VERIFY=false
|
# SIGNUPS_VERIFY=false
|
||||||
@ -489,7 +490,7 @@
|
|||||||
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
## Maximum attempts before an email token is reset and a new email will need to be sent.
|
||||||
# EMAIL_ATTEMPTS_LIMIT=3
|
# EMAIL_ATTEMPTS_LIMIT=3
|
||||||
##
|
##
|
||||||
## Setup email 2FA regardless of any organization policy
|
## Setup email 2FA on registration regardless of any organization policy
|
||||||
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
# EMAIL_2FA_ENFORCE_ON_VERIFIED_INVITE=false
|
||||||
## Automatically setup email 2FA as fallback provider when needed
|
## Automatically setup email 2FA as fallback provider when needed
|
||||||
# EMAIL_2FA_AUTO_FALLBACK=false
|
# EMAIL_2FA_AUTO_FALLBACK=false
|
||||||
|
@ -70,18 +70,31 @@ pub fn routes() -> Vec<rocket::Route> {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RegisterData {
|
pub struct RegisterData {
|
||||||
email: String,
|
email: String,
|
||||||
|
|
||||||
kdf: Option<i32>,
|
kdf: Option<i32>,
|
||||||
kdf_iterations: Option<i32>,
|
kdf_iterations: Option<i32>,
|
||||||
kdf_memory: Option<i32>,
|
kdf_memory: Option<i32>,
|
||||||
kdf_parallelism: Option<i32>,
|
kdf_parallelism: Option<i32>,
|
||||||
|
|
||||||
|
#[serde(alias = "userSymmetricKey")]
|
||||||
key: String,
|
key: String,
|
||||||
|
#[serde(alias = "userAsymmetricKeys")]
|
||||||
keys: Option<KeysData>,
|
keys: Option<KeysData>,
|
||||||
|
|
||||||
master_password_hash: String,
|
master_password_hash: String,
|
||||||
master_password_hint: Option<String>,
|
master_password_hint: Option<String>,
|
||||||
|
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
token: Option<String>,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
organization_user_id: Option<MembershipId>,
|
organization_user_id: Option<MembershipId>,
|
||||||
|
|
||||||
|
// Used only from the register/finish endpoint
|
||||||
|
email_verification_token: Option<String>,
|
||||||
|
accept_emergency_access_id: Option<EmergencyAccessId>,
|
||||||
|
accept_emergency_access_invite_token: Option<String>,
|
||||||
|
#[serde(alias = "token")]
|
||||||
|
org_invite_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -124,13 +137,78 @@ async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbCon
|
|||||||
|
|
||||||
#[post("/accounts/register", data = "<data>")]
|
#[post("/accounts/register", data = "<data>")]
|
||||||
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
_register(data, conn).await
|
_register(data, false, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult {
|
pub async fn _register(data: Json<RegisterData>, email_verification: bool, mut conn: DbConn) -> JsonResult {
|
||||||
let data: RegisterData = data.into_inner();
|
let mut data: RegisterData = data.into_inner();
|
||||||
let email = data.email.to_lowercase();
|
let email = data.email.to_lowercase();
|
||||||
|
|
||||||
|
let mut email_verified = false;
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
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)) => {
|
||||||
|
let claims = decode_invite(org_invite_token)?;
|
||||||
|
|
||||||
|
if claims.email != data.email {
|
||||||
|
err!("Claim email does not match email")
|
||||||
|
}
|
||||||
|
|
||||||
|
if &claims.member_id != organization_user_id {
|
||||||
|
err!("Claim org_user_id does not match organization_user_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
email_verified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
err!("Registration is missing required parameters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
// 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
|
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
||||||
if let Some(ref name) = data.name {
|
if let Some(ref name) = data.name {
|
||||||
@ -144,20 +222,17 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
|||||||
let password_hint = clean_password_hint(&data.master_password_hint);
|
let password_hint = clean_password_hint(&data.master_password_hint);
|
||||||
enforce_password_hint_setting(&password_hint)?;
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
let mut verified_by_invite = false;
|
|
||||||
|
|
||||||
let mut user = match User::find_by_mail(&email, &mut conn).await {
|
let mut user = match User::find_by_mail(&email, &mut conn).await {
|
||||||
Some(mut user) => {
|
Some(user) => {
|
||||||
if !user.password_hash.is_empty() {
|
if !user.password_hash.is_empty() {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(token) = data.token {
|
if let Some(token) = data.org_invite_token {
|
||||||
let claims = decode_invite(&token)?;
|
let claims = decode_invite(&token)?;
|
||||||
if claims.email == email {
|
if claims.email == email {
|
||||||
// Verify the email address when signing up via a valid invite token
|
// Verify the email address when signing up via a valid invite token
|
||||||
verified_by_invite = true;
|
email_verified = true;
|
||||||
user.verified_at = Some(Utc::now().naive_utc());
|
|
||||||
user
|
user
|
||||||
} else {
|
} else {
|
||||||
err!("Registration email does not match invite email")
|
err!("Registration email does not match invite email")
|
||||||
@ -181,7 +256,10 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
|||||||
// Order is important here; the invitation check must come first
|
// Order is important here; the invitation check must come first
|
||||||
// because the vaultwarden admin can invite anyone, regardless
|
// because the vaultwarden admin can invite anyone, regardless
|
||||||
// of other signup restrictions.
|
// 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())
|
User::new(email.clone())
|
||||||
} else {
|
} else {
|
||||||
err!("Registration not allowed or user already exists")
|
err!("Registration not allowed or user already exists")
|
||||||
@ -216,8 +294,12 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
|||||||
user.public_key = Some(keys.public_key);
|
user.public_key = Some(keys.public_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if email_verified {
|
||||||
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
|
}
|
||||||
|
|
||||||
if CONFIG.mail_enabled() {
|
if CONFIG.mail_enabled() {
|
||||||
if CONFIG.signups_verify() && !verified_by_invite {
|
if CONFIG.signups_verify() && !email_verified {
|
||||||
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
||||||
error!("Error sending welcome email: {:#?}", e);
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
}
|
}
|
||||||
@ -226,7 +308,7 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
|
|||||||
error!("Error sending welcome email: {:#?}", e);
|
error!("Error sending welcome email: {:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if verified_by_invite && is_email_2fa_required(data.organization_user_id, &mut conn).await {
|
if email_verified && is_email_2fa_required(data.organization_user_id, &mut conn).await {
|
||||||
email::activate_email_2fa(&user, &mut conn).await.ok();
|
email::activate_email_2fa(&user, &mut conn).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -205,6 +205,9 @@ fn config() -> Json<Value> {
|
|||||||
feature_states.insert("key-rotation-improvements".to_string(), true);
|
feature_states.insert("key-rotation-improvements".to_string(), true);
|
||||||
feature_states.insert("flexible-collections-v-1".to_string(), false);
|
feature_states.insert("flexible-collections-v-1".to_string(), false);
|
||||||
|
|
||||||
|
feature_states.insert("email-verification".to_string(), true);
|
||||||
|
feature_states.insert("unauth-ui-refresh".to_string(), true);
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
// Note: The clients use this version to handle backwards compatibility concerns
|
// Note: The clients use this version to handle backwards compatibility concerns
|
||||||
// This means they expect a version that closely matches the Bitwarden server version
|
// This means they expect a version that closely matches the Bitwarden server version
|
||||||
|
@ -24,7 +24,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login, prelogin, identity_register]
|
routes![login, prelogin, identity_register, register_verification_email, register_finish]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/connect/token", data = "<data>")]
|
#[post("/connect/token", data = "<data>")]
|
||||||
@ -714,7 +714,68 @@ async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
|
|||||||
|
|
||||||
#[post("/accounts/register", data = "<data>")]
|
#[post("/accounts/register", data = "<data>")]
|
||||||
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
_register(data, conn).await
|
_register(data, false, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct RegisterVerificationData {
|
||||||
|
email: String,
|
||||||
|
name: Option<String>,
|
||||||
|
// receiveMarketingEmails: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(rocket::Responder)]
|
||||||
|
enum RegisterVerificationResponse {
|
||||||
|
NoContent(()),
|
||||||
|
Token(Json<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/register/send-verification-email", data = "<data>")]
|
||||||
|
async fn register_verification_email(
|
||||||
|
data: Json<RegisterVerificationData>,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> ApiResult<RegisterVerificationResponse> {
|
||||||
|
let data = data.into_inner();
|
||||||
|
|
||||||
|
if !CONFIG.is_signup_allowed(&data.email) {
|
||||||
|
err!("Registration not allowed or user already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_send_mail = CONFIG.mail_enabled() && CONFIG.signups_verify();
|
||||||
|
|
||||||
|
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
|
||||||
|
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 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, &token).await?;
|
||||||
|
|
||||||
|
Ok(RegisterVerificationResponse::NoContent(()))
|
||||||
|
} else {
|
||||||
|
// If email verification is not required, return the token directly
|
||||||
|
// the clients will use this token to finish the registration
|
||||||
|
Ok(RegisterVerificationResponse::Token(Json(token)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/register/finish", data = "<data>")]
|
||||||
|
async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
|
||||||
|
_register(data, true, conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
|
// https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts
|
||||||
|
32
src/auth.rs
32
src/auth.rs
@ -35,6 +35,7 @@ static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.
|
|||||||
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
static JWT_SEND_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|send", CONFIG.domain_origin()));
|
||||||
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
static JWT_ORG_API_KEY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|api.organization", CONFIG.domain_origin()));
|
||||||
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
static JWT_FILE_DOWNLOAD_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|file_download", CONFIG.domain_origin()));
|
||||||
|
static JWT_REGISTER_VERIFY_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|register_verify", CONFIG.domain_origin()));
|
||||||
|
|
||||||
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
static PRIVATE_RSA_KEY: OnceCell<EncodingKey> = OnceCell::new();
|
||||||
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
static PUBLIC_RSA_KEY: OnceCell<DecodingKey> = OnceCell::new();
|
||||||
@ -145,6 +146,10 @@ pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> {
|
|||||||
decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())
|
decode_jwt(token, JWT_FILE_DOWNLOAD_ISSUER.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode_register_verify(token: &str) -> Result<RegisterVerifyClaims, Error> {
|
||||||
|
decode_jwt(token, JWT_REGISTER_VERIFY_ISSUER.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LoginJwtClaims {
|
pub struct LoginJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
@ -315,6 +320,33 @@ pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RegisterVerifyClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: String,
|
||||||
|
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_register_verify_claims(email: String, name: Option<String>, verified: bool) -> RegisterVerifyClaims {
|
||||||
|
let time_now = Utc::now();
|
||||||
|
RegisterVerifyClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + TimeDelta::try_minutes(30).unwrap()).timestamp(),
|
||||||
|
iss: JWT_REGISTER_VERIFY_ISSUER.to_string(),
|
||||||
|
sub: email,
|
||||||
|
name,
|
||||||
|
verified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct BasicJwtClaims {
|
pub struct BasicJwtClaims {
|
||||||
// Not before
|
// Not before
|
||||||
|
@ -484,7 +484,8 @@ make_config! {
|
|||||||
disable_icon_download: bool, true, def, false;
|
disable_icon_download: bool, true, def, false;
|
||||||
/// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
|
/// Allow new signups |> Controls whether new users can register. Users can be invited by the vaultwarden admin even if this is disabled
|
||||||
signups_allowed: bool, true, def, true;
|
signups_allowed: bool, true, def, true;
|
||||||
/// Require email verification on signups. This will prevent logins from succeeding until the address has been verified
|
/// Require email verification on signups. On new client versions, this will require verification at signup time. On older clients,
|
||||||
|
/// this will prevent logins from succeeding until the address has been verified
|
||||||
signups_verify: bool, true, def, false;
|
signups_verify: bool, true, def, false;
|
||||||
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
/// If signups require email verification, automatically re-send verification email if it hasn't been sent for a while (in seconds)
|
||||||
signups_verify_resend_time: u64, true, def, 3_600;
|
signups_verify_resend_time: u64, true, def, 3_600;
|
||||||
@ -734,7 +735,7 @@ make_config! {
|
|||||||
email_expiration_time: u64, true, def, 600;
|
email_expiration_time: u64, true, def, 600;
|
||||||
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
|
||||||
email_attempts_limit: u64, true, def, 3;
|
email_attempts_limit: u64, true, def, 3;
|
||||||
/// Automatically enforce at login |> Setup email 2FA provider regardless of any organization policy
|
/// Setup email 2FA at signup |> Setup email 2FA provider on registration regardless of any organization policy
|
||||||
email_2fa_enforce_on_verified_invite: bool, true, def, false;
|
email_2fa_enforce_on_verified_invite: bool, true, def, false;
|
||||||
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
|
/// Auto-enable 2FA (Know the risks!) |> Automatically setup email 2FA as fallback provider when needed
|
||||||
email_2fa_auto_fallback: bool, true, def, false;
|
email_2fa_auto_fallback: bool, true, def, false;
|
||||||
@ -1386,6 +1387,7 @@ where
|
|||||||
reg!("email/protected_action", ".html");
|
reg!("email/protected_action", ".html");
|
||||||
reg!("email/pw_hint_none", ".html");
|
reg!("email/pw_hint_none", ".html");
|
||||||
reg!("email/pw_hint_some", ".html");
|
reg!("email/pw_hint_some", ".html");
|
||||||
|
reg!("email/register_verify_email", ".html");
|
||||||
reg!("email/send_2fa_removed_from_org", ".html");
|
reg!("email/send_2fa_removed_from_org", ".html");
|
||||||
reg!("email/send_emergency_access_invite", ".html");
|
reg!("email/send_emergency_access_invite", ".html");
|
||||||
reg!("email/send_org_invite", ".html");
|
reg!("email/send_org_invite", ".html");
|
||||||
|
21
src/mail.rs
21
src/mail.rs
@ -201,6 +201,27 @@ pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
|
|||||||
send_email(address, &subject, body_html, body_text).await
|
send_email(address, &subject, body_html, body_text).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
None => err!("Failed to build verify URL query parameters"),
|
||||||
|
Some(query) => query,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/register_verify_email",
|
||||||
|
json!({
|
||||||
|
// `url.Url` would place the anchor `#` after the query parameters
|
||||||
|
"url": format!("{}/#/finish-signup/?{}", CONFIG.domain(), query_string),
|
||||||
|
"img_src": CONFIG._smtp_img_src(),
|
||||||
|
"email": email,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
send_email(email, &subject, body_html, body_text).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_welcome(address: &str) -> EmptyResult {
|
pub async fn send_welcome(address: &str) -> EmptyResult {
|
||||||
let (subject, body_html, body_text) = get_text(
|
let (subject, body_html, body_text) = get_text(
|
||||||
"email/welcome",
|
"email/welcome",
|
||||||
|
8
src/static/templates/email/register_verify_email.hbs
Normal file
8
src/static/templates/email/register_verify_email.hbs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Verify Your Email
|
||||||
|
<!---------------->
|
||||||
|
Verify this email address to finish creating your account by clicking the link below.
|
||||||
|
|
||||||
|
Verify Email Address Now: {{{url}}}
|
||||||
|
|
||||||
|
If you did not request to verify your account, you can safely ignore this email.
|
||||||
|
{{> email/email_footer_text }}
|
24
src/static/templates/email/register_verify_email.html.hbs
Normal file
24
src/static/templates/email/register_verify_email.html.hbs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
Verify Your Email
|
||||||
|
<!---------------->
|
||||||
|
{{> email/email_header }}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
Verify this email address to finish creating your account by clicking the link below.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
<a href="{{{url}}}"
|
||||||
|
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Verify Email Address Now
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||||
|
If you did not request to verify your account, you can safely ignore this email.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
@ -93,12 +93,19 @@ bit-nav-logo bit-nav-item .bwi-shield {
|
|||||||
/**** END Static Vaultwarden Changes ****/
|
/**** END Static Vaultwarden Changes ****/
|
||||||
/**** START Dynamic Vaultwarden Changes ****/
|
/**** START Dynamic Vaultwarden Changes ****/
|
||||||
{{#if signup_disabled}}
|
{{#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 */
|
/* Hide the register link on the login screen */
|
||||||
app-login form div + div + div + div + hr,
|
app-login form div + div + div + div + hr,
|
||||||
app-login form div + div + div + div + hr + p {
|
app-login form div + div + div + div + hr + p {
|
||||||
@extend %vw-hide;
|
@extend %vw-hide;
|
||||||
}
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#unless mail_enabled}}
|
{{#unless mail_enabled}}
|
||||||
/* Hide `Email` 2FA if mail is not enabled */
|
/* Hide `Email` 2FA if mail is not enabled */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user