From ef7ad6082cf06b3332a40d2c5fc18ccdb533fc4a Mon Sep 17 00:00:00 2001 From: Ginger Date: Sun, 22 Mar 2026 20:51:14 -0400 Subject: [PATCH] feat: Add support for registering a new account with an email address --- conduwuit-example.toml | 8 +++ src/api/client/account/register.rs | 108 +++++++++++++++++++++++------ src/api/router.rs | 1 + src/core/config/mod.rs | 14 ++++ 4 files changed, 110 insertions(+), 21 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index f201cb215..64c46b2ec 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -2068,3 +2068,11 @@ # - `address@domain.org` to not use a name # #sender = + +# Whether to require that users provide an email address when they register. +# +#require_email_for_registration = false + +# Whether to require that users who register with a registration token provide an email address. +# +#require_email_for_token_registration = false diff --git a/src/api/client/account/register.rs b/src/api/client/account/register.rs index e119b17df..1e69f81df 100644 --- a/src/api/client/account/register.rs +++ b/src/api/client/account/register.rs @@ -9,17 +9,22 @@ use conduwuit::{ }; use conduwuit_service::Services; use futures::{FutureExt, StreamExt}; +use lettre::{Address, message::Mailbox}; use register::RegistrationKind; use ruma::{ OwnedUserId, UserId, api::client::{ - account::register::{self, LoginType}, + account::{ + register::{self, LoginType}, + request_registration_token_via_email, + }, uiaa::{AuthFlow, AuthType}, }, events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent}, push, }; use serde_json::value::RawValue; +use service::mailer::messages; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; use crate::Ruma; @@ -391,13 +396,14 @@ pub(crate) async fn register_route( async fn create_registration_uiaa_session( services: &Services, ) -> Result<(Vec, Box)> { - let mut flows = vec![]; let mut params = HashMap::::new(); - if services.firstrun.is_first_run() { + let flows = if services.firstrun.is_first_run() { // Registration token forced while in first-run mode - flows.push(AuthFlow::new(vec![AuthType::RegistrationToken])); + vec![AuthFlow::new(vec![AuthType::RegistrationToken])] } else { + let mut flows = vec![]; + if services .registration_tokens .iterate_tokens() @@ -405,14 +411,25 @@ async fn create_registration_uiaa_session( .await .is_some() { - // Registration token flow is available - flows.push(AuthFlow::new(vec![AuthType::RegistrationToken])); + // Trusted registration flow with a token is available + let mut token_flow = AuthFlow::new(vec![AuthType::RegistrationToken]); + + if let Some(smtp) = &services.config.smtp + && smtp.require_email_for_token_registration + { + // Email is required for token registrations + token_flow.stages.push(AuthType::EmailIdentity); + } + + flows.push(token_flow); } + let mut untrusted_flow = AuthFlow::default(); + if services.config.recaptcha_private_site_key.is_some() { if let Some(pubkey) = &services.config.recaptcha_site_key { - // ReCaptcha flow is available - flows.push(AuthFlow::new(vec![AuthType::ReCaptcha])); + // ReCaptcha is configured for untrusted registrations + untrusted_flow.stages.push(AuthType::ReCaptcha); params.insert( AuthType::ReCaptcha.as_str().to_owned(), @@ -422,23 +439,36 @@ async fn create_registration_uiaa_session( ); } } - } - if flows.is_empty() { - // Registration is enabled, but no flows are configured. Bail out by default - // unless open registration was explicitly enabled. - if !services - .config - .yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse + if let Some(smtp) = &services.config.smtp + && smtp.require_email_for_registration { - return Err!(Request(Forbidden( - "This server is not accepting registrations at this time." - ))); + // Email is required for untrusted registrations + untrusted_flow.stages.push(AuthType::EmailIdentity); } - // We have open registration enabled (😧), provide a dummy flow - flows.push(AuthFlow { stages: vec![AuthType::Dummy] }); - } + if !untrusted_flow.stages.is_empty() { + flows.push(untrusted_flow); + } + + if flows.is_empty() { + // No flows are configured. Bail out by default + // unless open registration was explicitly enabled. + if !services + .config + .yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse + { + return Err!(Request(Forbidden( + "This server is not accepting registrations at this time." + ))); + } + + // We have open registration enabled (😧), provide a dummy flow + flows.push(AuthFlow::new(vec![AuthType::Dummy])); + } + + flows + }; let params = serde_json::value::to_raw_value(¶ms).expect("params should be valid JSON"); @@ -536,3 +566,39 @@ async fn determine_registration_user_id( } } } + +/// # `POST /_matrix/client/v3/register/email/requestToken` +/// +/// Requests a validation email for the purpose of registering a new account +pub(crate) async fn register_request_token_route( + State(services): State, + body: Ruma, +) -> Result { + let Ok(email) = Address::try_from(body.email.clone()) else { + return Err!(Request(InvalidParam("Invalid email address"))); + }; + + if services + .threepid + .get_localpart_for_email(&email) + .await + .is_some() + { + return Err!(Request(ThreepidInUse("This email address is already in use"))); + } + + let session = services + .threepid + .send_validation_email( + Mailbox::new(None, email), + |verification_link| messages::NewAccount { + server_name: services.config.server_name.as_ref(), + verification_link, + }, + &body.client_secret, + body.send_attempt.try_into().unwrap(), + ) + .await?; + + Ok(request_registration_token_via_email::v3::Response::new(session)) +} diff --git a/src/api/router.rs b/src/api/router.rs index 54e902ea3..66951bd83 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -29,6 +29,7 @@ pub fn build(router: Router, server: &Server) -> Router { .ruma_route(&client::get_supported_versions_route) .ruma_route(&client::get_register_available_route) .ruma_route(&client::register::register_route) + .ruma_route(&client::register::register_request_token_route) .ruma_route(&client::get_login_types_route) .ruma_route(&client::login_route) .ruma_route(&client::login_token_route) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index a5345b662..59f229122 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2477,6 +2477,20 @@ pub struct SmtpConfig { /// - `Name ` to specify a sender name /// - `address@domain.org` to not use a name pub sender: Mailbox, + + /// Whether to require that users provide an email address when they + /// register. + /// + /// default: false + #[serde(default)] + pub require_email_for_registration: bool, + + /// Whether to require that users who register with a registration token + /// provide an email address. + /// + /// default: false + #[serde(default)] + pub require_email_for_token_registration: bool, } const DEPRECATED_KEYS: &[&str] = &[